一步步打造一个简单的 MVC 电商网站 - BooksStore(三)
本系列的 GitHub地址:https://github.com/liqingwen2015/Wen.BooksStore
《一步步打造一个简单的 MVC 电商网站 - BooksStore(一)》
《一步步打造一个简单的 MVC 电商网站 - BooksStore(二)》
《一步步打造一个简单的 MVC 电商网站 - BooksStore(三)》
《一步步打造一个简单的 MVC 电商网站 - BooksStore(四)》
简介
上一节我们完成了两个主要功能:添加到购物车和分类导航,这一节我们会完成整个购物车的流程,以及订单处理。
该系列主要功能与知识点如下:
分类、产品浏览、购物车、结算、CRUD(增删改查) 管理、发邮件、分页、模型绑定、认证过滤器和单元测试等(预计剩余两篇,周三(因为周二不上班)先发布一篇)。
【备注】项目使用 VS2015 + C#6 进行开发,有问题请发表在留言区哦,还有,页面长得比较丑,请见谅。
目录
完成购物车
订单结算
一、完成购物车
上一节其实已经完成了移除购物车和清空购物车的方法,只是尚未将可供用户操作的按钮放在页面区域。除了增加这两个按钮,也会在页面顶部的位置增加购物车的摘要(用于显示用户的购物总额)。
下面是上一节已经写好的 CartController 代码。
/// <summary> /// 购物车 /// </summary> public class CartController : Controller { private readonly IBookRepository _bookRepository; public CartController(IBookRepository bookRepository) { _bookRepository = bookRepository; } /// <summary> /// 首页 /// </summary> /// <param name="returnUrl"></param> /// <returns></returns> public ViewResult Index(string returnUrl) { return View(new CartIndexViewModel() { Cart = GetCart(), ReturnUrl = returnUrl }); } /// <summary> /// 添加到购物车 /// </summary> /// <param name="id"></param> /// <param name="returnUrl"></param> /// <returns></returns> public RedirectToRouteResult AddToCart(int id, string returnUrl) { var book = _bookRepository.Books.FirstOrDefault(x => x.Id == id); if (book != null) { GetCart().AddBook(book, 1); } return RedirectToAction("Index", new { returnUrl }); } /// <summary> /// 从购物车移除 /// </summary> /// <param name="id"></param> /// <param name="returnUrl"></param> /// <returns></returns> public RedirectToRouteResult RemoveFromCart(int id, string returnUrl) { var book = _bookRepository.Books.FirstOrDefault(x => x.Id == id); if (book != null) { GetCart().RemoveBook(book); } return RedirectToAction("Index", new { returnUrl }); } /// <summary> /// 获取购物车 /// </summary> /// <returns></returns> private Cart GetCart() { var cart = (Cart)Session["Cart"]; if (cart != null) return cart; cart = new Cart(); Session["Cart"] = cart; return cart; } }
1.加入移除书籍和清空购物车的功能
Index.cshtml
@model Wen.BooksStore.WebUI.Models.CartIndexViewModel <h2>我的购物车</h2> <table class="table"> <thead> <tr> <th>书名</th> <th>价格</th> <th>数量</th> <th>总计</th> <th> </th> </tr> </thead> <tbody> @foreach (var item in Model.Cart.GetCartItems) { <tr> <td>@item.Book.Name</td> <td>@item.Book.Price</td> <td>@item.Quantity</td> <td>@((item.Book.Price * item.Quantity).ToString("C"))</td> <td> @using (Html.BeginForm("RemoveFromCart", "Cart")) { @Html.Hidden("id", item.Book.Id) @Html.HiddenFor(x => x.ReturnUrl) <input type="submit" value="- 移除" /> } </td> </tr> } <tr> <td> </td> <td> </td> <td>总计:</td> <td>@Model.Cart.ComputeTotalValue().ToString("C")</td> <td> @using (Html.BeginForm("Clear", "Cart")) { @Html.HiddenFor(x => x.ReturnUrl) <input type="submit" value="清空购物车" /> } </td> </tr> </tbody> </table>
【备注】@Html.Hidden("id", item.Book.Id) 是用于生成隐藏的字段,如果直接使用@Html.HiddenFor(),生成的 name 将会是 item.Book.Id ,将和 CartController 中 RemoveFromCart(int id, string return) 的参数不匹配。
显示的效果如下:
2.添加摘要:我们在购物车存放了许多东西,通过摘要,可以显示购物总额的缩略图,我们选择的位置在顶部右上角的一个比较显眼的位置进行显示它,当然,还需要有点击的跳转按钮方便显示所有的购物清单页面。
继续在 CartController 下新增一个 Action,名为 Summary,返回值是一个分部视图:
/// <summary> /// 摘要 /// </summary> /// <returns></returns> public PartialViewResult Summary() { return PartialView(GetCart()); }
对应的Summary.cshtml
@model Wen.BooksStore.Domain.Entities.Cart <div class="bookSummary"> 你的购物车:@Model.ComputeTotalValue() <span>@Html.ActionLink("结算", "Checkout", "Cart", new { retunUrl = Request.Url.PathAndQuery }, null)</span> </div>
对应的布局页_Layout.cshtml 修改的地方为:
_Layout.cshtml
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> <link rel="nofollow noopener noreferrer" href="~/Contents/Site.css" rel="stylesheet" /> </head> <body> <div id="header"> @{ Html.RenderAction("Summary", "Cart");} <div class="title">图书商城</div> </div> <div id="sideBar"> @{ Html.RenderAction("Sidebar", "Nav"); } </div> <div id="content"> @RenderBody() </div> </body> </html>
添加了新的东西,css 也要进行修改:
Site.css
body { } #header, #content, #sideBar { display: block; } #header { background-color: green; border-bottom: 2px solid #111; color: White; } #header, .title { font-size: 1.5em; padding: .5em; } #sideBar { float: left; width: 8em; padding: .3em; } #content { border-left: 2px solid gray; margin-left: 10em; padding: 1em; } .pager { text-align: right; padding: .5em 0 0 0; margin-top: 1em; } .pager A { font-size: 1.1em; color: #666; padding: 0 .4em 0 .4em; } .pager A:hover { background-color: Silver; } .pager A.selected { background-color: #353535; color: White; } .item input { float: right; color: White; background-color: green; } .table { width: 100%; padding: 0; margin: 0; } .table th { font: bold 12px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif; color: #4f6b72; border-right: 1px solid #C1DAD7; border-bottom: 1px solid #C1DAD7; border-top: 1px solid #C1DAD7; letter-spacing: 2px; text-transform: uppercase; text-align: left; padding: 6px 6px 6px 12px; background: #CAE8EA no-repeat; } .table td { border-right: 1px solid #C1DAD7; border-bottom: 1px solid #C1DAD7; background: #fff; font-size: 14px; padding: 6px 6px 6px 12px; color: #4f6b72; } .table td.alt { background: #F5FAFA; color: #797268; } .table th.spec, td.spec { border-left: 1px solid #C1DAD7; } .bookSummary { width: 15%; float: right; margin-top: 1.5%; }
二、订单结算
购物完毕就是结算页面了,这里的订单结算并不涉及支付接口的调用,只是使用邮件的形式进行通知而已。
这里,我设计结算的时候需要要求用户输入一些信息,如姓名、地址和邮箱等信息,在点击确定时我再将这些输入的信息与购物清单的信息从系统的邮箱发到你所输入的邮箱当中。一个比较直观的图:
1.在 Entities 中添加一个域模型 Contact.cs 表示联系人的信息。
/// <summary> /// 联系信息 /// </summary> public class Contact { [Required(ErrorMessage = "姓名不能为空")] public string Name { get; set; } [Required(ErrorMessage = "地址不能为空")] public string Address { get; set; } [Required(ErrorMessage = "邮箱不能为空")] [RegularExpression(@"(\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*\w\w)", ErrorMessage = "输入的邮箱地址不合法")] public string Email { get; set; } }
CartController.cs 添加一个用于结算的 Action:
/// <summary> /// 结算 /// </summary> /// <returns></returns> public ViewResult Checkout() { return View(new Contact()); }
Checkout.cshtml 中的:
@model Wen.BooksStore.Domain.Entities.Contact <div> @using (Html.BeginForm()) { <div class="error">@Html.ValidationSummary()</div> <div>姓名: @Html.TextBoxFor(x => x.Name)</div> <div>地址: @Html.TextBoxFor(x => x.Address)</div> <div>邮箱: @Html.TextBoxFor(x => x.Email)</div> <div><input type="submit" value="提交" /></div> } </div>
这里使用的是模型校验,_Layout.cshtml 布局页需要引入js:
<script src="~/Scripts/jquery-1.10.2.js"></script> <script src="~/Scripts/jquery.validate.js"></script> <script src="~/Scripts/jquery.validate.unobtrusive.js"></script>
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> <link rel="nofollow noopener noreferrer" href="~/Contents/Site.css" rel="stylesheet" /> <script src="~/Scripts/jquery-1.10.2.js"></script> <script src="~/Scripts/jquery.validate.js"></script> <script src="~/Scripts/jquery.validate.unobtrusive.js"></script> </head> <body> <div id="header"> @{ Html.RenderAction("Summary", "Cart");} <div class="title">图书商城</div> </div> <div id="sideBar"> @{ Html.RenderAction("Sidebar", "Nav"); } </div> <div id="content"> @RenderBody() </div> </body> </html>
尝试运行,会出现以下页面,如果信息不填的话会出现相关的错误提示:
2.接下来,要进入“提交”后的流程了。
现在还需要一个组件用于处理订单,创建一个用于订单处理的接口,和一个该接口的实现,再通过 Ninject 进行两者的绑定:
/// <summary> /// 订单处理 /// </summary> public interface IOrderProcessor { /// <summary> /// 处理订单 /// </summary> /// <param name="cart"></param> /// <param name="contact"></param> void ProcessOrder(Cart cart, Contact contact); }
建立一个实现该接口用于处理订单的实体类,这里并不是调用支付接口,而是简单通过 BCL 中的进行邮件的发送。
EmailOrderProcessor.cs:
/// <summary> /// 邮件订单处理器 /// </summary> public class EmailOrderProcessor : IOrderProcessor { /// <summary> /// 发送人 /// </summary> public static class Sender { /// <summary> /// 账号 /// </summary> public static string Account = "你的@qq.com"; /// <summary> /// 密码 /// </summary> public static string Password = "xxx"; } /// <summary> /// 处理订单 /// </summary> /// <param name="cart"></param> /// <param name="contact"></param> public void ProcessOrder(Cart cart, Contact contact) { if (string.IsNullOrEmpty(contact.Email)) { throw new Exception("Email 不能为空!"); } var sb = new StringBuilder(); foreach (var item in cart.GetCartItems) { sb.AppendLine($"《{item.Book.Name}》:{item.Book.Price} * {item.Quantity} = {item.Book.Price * item.Quantity}"); } sb.AppendLine($"总额:{cart.GetCartItems.Sum(x => x.Quantity * x.Book.Price)}"); sb.AppendLine(); sb.AppendLine($"联系人:{contact.Name} {contact.Address}"); //设置发件人,发件人需要与设置的邮件发送服务器的邮箱一致 var fromAddr = new MailAddress(Sender.Account); var message = new MailMessage { From = fromAddr }; //设置收件人,可添加多个,添加方法与下面的一样 message.To.Add(contact.Email); //设置抄送人 message.CC.Add(Sender.Account); //设置邮件标题 message.Subject = "您的订单正在出库..."; //设置邮件内容 message.Body = sb.ToString(); //设置邮件发送服务器,服务器根据你使用的邮箱而不同,可以到相应的 邮箱管理后台查看,下面是QQ的 var client = new SmtpClient("smtp.qq.com", 25) { Credentials = new NetworkCredential(Sender.Account, Sender.Password), EnableSsl = true }; //设置发送人的邮箱账号和密码 //启用ssl,也就是安全发送 //发送邮件 client.Send(message); }
CartController 也需要稍作调整:
还要在 CartController 中额外添加一个带[HttPost] 特性的名为 Checkout 方法:
/// <summary> /// 结算 /// </summary> /// <param name="contact"></param> /// <returns></returns> [HttpPost] public ViewResult Checkout(Contact contact) { if (!ModelState.IsValid) return View(contact); var cart = GetCart(); _orderProcessor.ProcessOrder(cart, contact); cart.Clear(); return View("Thanks"); }
当校验成功时,会调用接口发一条信息,并且清空已有的购物车,然后跳转到指定的一个新视图页:
新建 Thanks.cshtml,内容如下:
Thanks
别忘了添加绑定哦,使用 DI 容器将两者进行绑定:
启动页面,试试效果吧:
看来,好像成功了哦:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。