ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(六)学生借阅/预约/查询书籍事务
前言:
本系列文章主要为我之前所学知识的一次微小的实践,以我学校图书馆管理系统为雏形所作。
本系列文章主要参考资料:
微软文档:https://docs.microsoft.com/zh-cn/aspnet/core/getting-started/?view=aspnetcore-2.1&tabs=windows
《Pro ASP.NET MVC 5》、《锋利的 jQuery》
此系列皆使用 VS2017+C# 作为开发环境。如果有什么问题或者意见欢迎在留言区进行留言。
项目 github 地址:https://github.com/NanaseRuri/LibraryDemo
本章内容:自定义布局页、自定义 EditorFor 模板、EF 多对多数据的更新
一、自定义布局页
在 ASP.NET 中,默认将 HTML 页面的 body 元素一部分抽出来,该部分称作 RenderBody ;然后将这部分放到一个布局即大体页面框架中即可完成对同一系列的页面进行精简的布局实现。
默认布局页为 _Layout.cshtml,可在视图文件夹中根目录或各个控制器视图目录的 _ViewStart.cshtml 修改默认布局页,或者在每个 Razor 页面的开头中指定布局页:
@{ ViewData["Title"] = "EditLendingInfo"; Layout = "_LendingLayout"; }
之前一直使用的是 VS 的默认布局页,现在以该默认布局页为基础,添加自己所需要的信息:
1 @using Microsoft.AspNetCore.Http.Extensions 2 @using Microsoft.AspNetCore.Authorization 3 @inject IAuthorizationService AuthorizationService 4 <!DOCTYPE html> 5 <html> 6 <head> 7 <meta charset="utf-8" /> 8 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 9 <title>@ViewData["Title"] - LibraryDemo</title> 10 <environment include="Development"> 11 <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" /> 12 <link rel="stylesheet" href="~/css/site.css" /> 13 </environment> 14 <environment exclude="Development"> 15 <link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css" 16 asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css" 17 asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" /> 18 <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" /> 19 </environment> 20 </head> 21 <body> 22 <nav class="navbar navbar-inverse navbar-fixed-top"> 23 <div class="container"> 24 <div class="navbar-header"> 25 <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> 26 <span class="sr-only">Toggle navigation</span> 27 <span class="icon-bar"></span> 28 <span class="icon-bar"></span> 29 <span class="icon-bar"></span> 30 </button> 31 <a asp-area="" asp-controller="BookInfo" asp-action="Index" class="navbar-brand">LibraryDemo</a> 32 </div> 33 <div class="navbar-collapse collapse"> 34 <ul class="nav navbar-nav"> 35 <li><a asp-area="" asp-controller="BookInfo" asp-action="Index">首页</a></li> 36 <li> 37 @if (User.Identity.IsAuthenticated) 38 { 39 <a asp-controller="BookInfo" asp-action="PersonalInfo">@User.Identity.Name</a> 40 } 41 else 42 { 43 <a asp-area="" asp-controller="StudentAccount" asp-action="Login" 44 asp-route-returnUrl="@(Context.Request.GetDisplayUrl())">登录</a> 45 } 46 </li> 47 <li><a asp-area="" asp-controller="BookInfo" asp-action="Recommend">推荐图书</a></li> 48 <li><a href="mailto:Nanase@cnblogs.com">联系我们</a></li> 49 @if (User.Identity.IsAuthenticated) 50 { 51 <li> 52 <a asp-action="Logout" asp-controller="StudentAccount" asp-route-returnUrl="@(Context.Request.GetDisplayUrl())">注销</a> 53 </li> 54 } 55 </ul> 56 </div> 57 </div> 58 </nav> 59 <div align="center"> 60 <br /> 61 <form action="@Url.Action("Search", "BookInfo")"> 62 @Html.DropDownList("keyword", new List<SelectListItem>() 63 { 64 new SelectListItem("书名", "Name"), 65 new SelectListItem("ISBN", "ISBN"), 66 new SelectListItem("索书号", "FetchBookNumber"), 67 }) 68 <input type="text" name="value"/> 69 <button type="submit"><span class="glyphicon glyphicon-search"></span></button> 70 </form> 71 72 @if (TempData["message"] != null) 73 { 74 <br/> 75 <p class="text-success">@TempData["message"]</p> 76 <br/> 77 } 78 </div> 79 <partial name="_CookieConsentPartial" /> 80 <div class="container body-content"> 81 82 @RenderBody() 83 <hr /> 84 </div> 85 86 <div class="container" style="margin-top: 20px;"> 87 <footer> 88 <p>© 2018 - LibraryDemo</p> 89 </footer> 90 </div> 91 <environment include="Development"> 92 <script src="~/lib/jquery/dist/jquery.js"></script> 93 <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script> 94 <script src="~/js/site.js" asp-append-version="true"></script> 95 </environment> 96 <environment exclude="Development"> 97 <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.3.1.min.js" 98 asp-fallback-src="~/lib/jquery/dist/jquery.min.js" 99 asp-fallback-test="window.jQuery" 100 crossorigin="anonymous" 101 integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT"></script> 102 <script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js" 103 asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js" 104 asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal" 105 crossorigin="anonymous" 106 integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"></script> 107 <script src="~/js/site.min.js" asp-append-version="true"></script> 108 </environment> 109 @RenderSection("Scripts", required: false) 110 </body> 111 </html>
现在大体框架:
除了默认的 RenderBody 外,可以指定特定的部分放在页面的不同地方,在布局页中使用@RenderSection("SectionName"):
@RenderSection("SectionName")
且在视图页中使用指定特定的节@section SectionName{ };
1 @section SectionName{ 2 3 };
则该视图页中的 SectionName 部分会被提取出来放到布局页对应的位置。
二、管理员编辑借阅信息
动作方法:
在此对数据库的表格使用 Include 方法使 EF 应用其导航属性以获得 KeepingBooks 列表,否则使用 Student 对象 KeepingBooks 属性只会返回空。
1 [Authorize(Roles = "Admin")] 2 public IActionResult EditLendingInfo(string barcode) 3 { 4 if (barcode == null) 5 { 6 return RedirectToAction("BookDetails"); 7 } 8 Book book = _lendingInfoDbContext.Books.FirstOrDefault(b => b.BarCode == barcode); 9 return View(book); 10 } 11 12 [HttpPost] 13 [Authorize(Roles = "Admin")] 14 [ValidateAntiForgeryToken] 15 public async Task<IActionResult> EditLendingInfo([Bind("BarCode,ISBN,BorrowTime,KeeperId,AppointedLatestTime,State")]Book book) 16 { 17 if (ModelState.IsValid) 18 { 19 if (book.BorrowTime > DateTime.Now) 20 { 21 ModelState.AddModelError("", "请检查外借时间"); 22 return View(book); 23 } 24 if (book.AppointedLatestTime.HasValue) 25 { 26 if (book.AppointedLatestTime < DateTime.Now) 27 { 28 ModelState.AddModelError("", "请检查预约时间"); 29 return View(book); 30 } 31 32 if (book.KeeperId == null) 33 { 34 ModelState.AddModelError("", "不存在该学生"); 35 return View(book); 36 } 37 } 38 39 StudentInfo student = await _lendingInfoDbContext.Students.Include(s => s.KeepingBooks).FirstOrDefaultAsync(s => s.UserName == book.KeeperId); 40 41 Book addedBook = _lendingInfoDbContext.Books 42 .Include(b => b.Keeper).ThenInclude(k => k.KeepingBooks) 43 .FirstOrDefault(b => b.BarCode == book.BarCode); 44 if (addedBook == null) 45 { 46 return RedirectToAction("Books", new { isbn = book.ISBN }); 47 } 48 49 StudentInfo preStudent = addedBook.Keeper; 50 AppointmentOrLending targetLending = 51 preStudent?.KeepingBooks.FirstOrDefault(b => b.BookId == addedBook.BarCode); 52 53 addedBook.AppointedLatestTime = book.AppointedLatestTime; 54 addedBook.State = book.State; 55 addedBook.BorrowTime = book.BorrowTime; 56 addedBook.MatureTime = null; 57 58 preStudent?.KeepingBooks.Remove(targetLending); 59 60 if (addedBook.BorrowTime.HasValue) 61 { 62 if (book.KeeperId == null) 63 { 64 ModelState.AddModelError("", "请检查借阅者"); 65 return View(book); 66 } 67 68 if (student == null) 69 { 70 ModelState.AddModelError("", "不存在该学生"); 71 return View(book); 72 } 73 if (student != null) 74 { 75 if (student.KeepingBooks.Count >= student.MaxBooksNumber) 76 { 77 TempData["message"] = "该学生借书已超过上限"; 78 } 79 80 addedBook.State = BookState.Borrowed; 81 student.KeepingBooks.Add(new AppointmentOrLending() 82 { 83 BookId = addedBook.BarCode, 84 StudentId = student.UserName 85 }); 86 addedBook.Keeper = student; 87 88 } 89 addedBook.MatureTime = addedBook.BorrowTime + TimeSpan.FromDays(28); 90 } 91 92 93 TempData["message"] = "保存成功"; 94 await _lendingInfoDbContext.SaveChangesAsync(); 95 return RedirectToAction("Books", new { isbn = book.ISBN }); 96 } 97 return View(book); 98 }
将 BookState 枚举提取成分部视图 _BookStatePartial:
1 @using LibraryDemo.Models.DomainModels 2 @model Book 3 <div class="form-group"> 4 @Html.LabelFor(b => b.State) 5 @Html.DropDownListFor(b => b.State, Enum.GetValues(typeof(BookState)).Cast<Enum>().Select(state => 6 { 7 string enumVal = Enum.GetName(typeof(BookState), state); 8 string displayVal; 9 switch (enumVal) 10 { 11 case "Normal": 12 displayVal = "可借阅"; 13 break; 14 case "Readonly": 15 displayVal = "馆内阅览"; 16 break; 17 case "Borrowed": 18 displayVal = "已借出"; 19 break; 20 case "ReBorrowed": 21 displayVal = "被续借"; 22 break; 23 case "Appointed": 24 displayVal = "被预约"; 25 break; 26 default: 27 displayVal = ""; 28 break; 29 } 30 return new SelectListItem() 31 { 32 Text = displayVal, 33 Value = enumVal, 34 Selected = Model.State.ToString() == enumVal 35 }; 36 })) 37 </div>
Html.DisplayFor 方法是 ASP.NET 内置对各种属性进行展示的方法,可以在项目的 Views 文件夹中的 Shared 文件夹创建对应类型的 Editor 模板供其使用:
在此创建一个 DateTime.cshtml,于是我们使用 Html.DisplayFor 用于展示 DateTime 数据时只会显示年份/月份/天数:
1 @model DateTime? 2 3 @Model?.ToString("yyyy/M/dd")
视图中第 40 行使用 partial TagHelper 指定其 name 为 _BookStatePartial 以应用分部视图:
1 @model LibraryDemo.Models.DomainModels.Book 2 @{ 3 ViewData["Title"] = "EditLendingInfo"; 4 Layout="_LendingLayout"; 5 } 6 7 <h2>@Model.BarCode</h2> 8 <h3>@Model.Name</h3> 9 <br/> 10 11 <script> 12 window.onload = function() { 13 $("input").addClass("form-control"); 14 } 15 window.onbeforeunload = function (event) { 16 return "您的数据未保存,确定退出?"; 17 } 18 function removeOnbeforeunload() { 19 window.onbeforeunload = ""; 20 } 21 </script> 22 23 @Html.ValidationSummary(false,"",new{@class="text-danger"}) 24 25 <form asp-action="EditLendingInfo" method="post"> 26 @Html.HiddenFor(b => b.BarCode) 27 @Html.HiddenFor(b => b.ISBN) 28 <div class="form-group"> 29 @Html.LabelFor(b => b.KeeperId) 30 @Html.EditorFor(b => b.KeeperId) 31 </div> 32 <div class="form-group"> 33 @Html.LabelFor(b => b.BorrowTime) 34 @Html.EditorFor(b => b.BorrowTime) 35 </div> 36 <div class="form-group"> 37 @Html.LabelFor(b => b.AppointedLatestTime) 38 @Html.EditorFor(b => b.AppointedLatestTime) 39 </div> 40 <partial model="@Model" name="_BookStatePartial"/> 41 <input type="submit" onclick="return removeOnbeforeunload()" class="btn-primary"/> 42 </form>
结果:
三、查看个人信息
这里通过 User.Identity.Name 获取当前登录人的信息以选定当前登录的学生:
1 [Authorize] 2 public async Task<IActionResult> PersonalInfo() 3 { 4 StudentInfo student = await _lendingInfoDbContext.Students.Include(s => s.KeepingBooks).ThenInclude(k => k.Book) 5 .FirstOrDefaultAsync(s => s.UserName == User.Identity.Name); 6 decimal fine = 0; 7 foreach (var book in student.KeepingBooks.Where(b => b.Book.MatureTime < DateTime.Now && !b.AppointingDateTime.HasValue)) 8 { 9 fine += (DateTime.Now - book.Book.MatureTime.Value).Days * (decimal)0.2; 10 book.Book.State = book.Book.State == BookState.Appointed ? BookState.Appointed : BookState.Expired; 11 } 12 13 student.Fine = fine; 14 PersonalInfoViewModel model = new PersonalInfoViewModel() 15 { 16 Student = student, 17 BookingBook = _lendingInfoDbContext.Books.FirstOrDefault(b => b.BarCode == student.AppointingBookBarCode) 18 }; 19 return View(model); 20 }
视图:
1 @model LibraryDemo.Models.PersonalInfoViewModel 2 @{ 3 ViewData["Title"] = "PersonalInfo"; 4 Layout = "_LendingLayout"; 5 } 6 <link rel="stylesheet" href="~/css/BookInfo.css" /> 7 <script> 8 function ensureCancel() { 9 if (confirm("确定取消预约?")) { 10 return true; 11 } 12 return false; 13 } 14 </script> 15 16 <h2>@Model.Student.Name</h2> 17 <br /> 18 @if (Model.Student.KeepingBooks.Any(b => b.Book.MatureTime < DateTime.Now)) 19 { 20 <table> 21 <thead> 22 <tr> 23 <th colspan="5">过期书籍</th> 24 </tr> 25 </thead> 26 <tr> 27 <th>书名</th> 28 <th>条形码</th> 29 <th>状态</th> 30 <th>到期时间</th> 31 <th>索书号</th> 32 </tr> 33 34 @foreach (var matureBook in Model.Student.KeepingBooks.Where(b => b.Book.MatureTime < DateTime.Now && !b.AppointingDateTime.HasValue)) 35 { 36 <tr> 37 <td>@matureBook.Book.Name</td> 38 <td>@matureBook.Book.BarCode</td> 39 <td>@Html.DisplayFor(b => matureBook.Book.State)</td> 40 <td>@matureBook.Book.MatureTime?.ToString("yyyy/MM/dd")</td> 41 <td>@matureBook.Book.FetchBookNumber</td> 42 </tr> 43 } 44 <tfoot><tr><td colspan="5">罚款:@Model.Student.Fine</td></tr></tfoot> 45 </table> 46 } 47 <form asp-action="ReBorrow" method="post"> 48 <table> 49 <tr> 50 <th>续借</th> 51 <th>书名</th> 52 <th>条形码</th> 53 <th>状态</th> 54 <th>到期时间</th> 55 <th>索书号</th> 56 </tr> 57 @if (!Model.Student.KeepingBooks.Any()) 58 { 59 <tr> 60 <td colspan="6" style="text-align: center">未借阅书本</td> 61 </tr> 62 } 63 else 64 { 65 foreach (var keepingBook in Model.Student.KeepingBooks.Where(b=>!b.AppointingDateTime.HasValue)) 66 { 67 <tr> 68 <td><input type="checkbox" value="@keepingBook.Book.BarCode" name="barcodes"/></td> 69 <td>@keepingBook.Book.Name</td> 70 <td>@keepingBook.Book.BarCode</td> 71 <td>@Html.DisplayFor(b=>keepingBook.Book.State)</td> 72 <td>@keepingBook.Book.MatureTime?.ToString("yyyy/MM/dd")</td> 73 <td>@keepingBook.Book.FetchBookNumber</td> 74 </tr> 75 } 76 } 77 </table> 78 <br/> 79 <input type="submit" class="btn-primary btn" value="续借"/> 80 </form> 81 82 <br /> 83 @if (Model.BookingBook != null) 84 { 85 <form asp-action="CancelAppointing"> 86 <table> 87 <tr> 88 <th>书名</th> 89 <th>条形码</th> 90 <th>状态</th> 91 <th>预约时间</th> 92 <th>索书号</th> 93 </tr> 94 <book-info book="@Model.BookingBook" is-booking-book="true"></book-info> 95 </table> 96 <br /> 97 <input type="hidden" name="barcode" value="@Model.BookingBook.BarCode"/> 98 <input type="submit" value="取消预约" class="btn btn-danger" onclick="return ensureCancel()"/> 99 </form> 100 }
结果:
四、借阅书籍
由于暂时未有获取二维码的接口,仅通过直接访问 Lending 模拟借阅:
1 [Authorize] 2 public async Task<IActionResult> Lending(string barcode) 3 { 4 Book targetBook=await _lendingInfoDbContext.Books.Include(b=>b.Appointments).FirstOrDefaultAsync(b => b.BarCode == barcode); 5 if (targetBook==null) 6 { 7 TempData["message"] = "请重新扫描书籍"; 8 return RedirectToAction("PersonalInfo"); 9 } 10 11 if (targetBook.Appointments.Any(a=>a.AppointingDateTime.HasValue)) 12 { 13 TempData["message"] = "此书已被预约"; 14 return RedirectToAction("PersonalInfo"); 15 } 16 17 if (targetBook.State==BookState.Readonly) 18 { 19 TempData["message"] = "此书不供外借"; 20 return RedirectToAction("PersonalInfo"); 21 } 22 23 targetBook.State = BookState.Borrowed; 24 targetBook.BorrowTime = DateTime.Now.Date; 25 targetBook.MatureTime = DateTime.Now.Date+TimeSpan.FromDays(28); 26 StudentInfo student = 27 await _lendingInfoDbContext.Students.Include(s=>s.KeepingBooks).FirstOrDefaultAsync(s => s.UserName == User.Identity.Name); 28 student.KeepingBooks.Add(new AppointmentOrLending() 29 { 30 BookId = targetBook.BarCode, 31 StudentId = student.UserName 32 }); 33 await _lendingInfoDbContext.SaveChangesAsync(); 34 TempData["message"] = "借书成功"; 35 return RedirectToAction("PersonalInfo"); 36 }
结果:
六、续借书籍
动作方法:
1 [Authorize] 2 [HttpPost] 3 public async Task<IActionResult> ReBorrow(IEnumerable<string> barcodes) 4 { 5 StringBuilder borrowSuccess = new StringBuilder(); 6 StringBuilder borrowFail = new StringBuilder(); 7 borrowSuccess.Append("成功续借书籍:"); 8 borrowFail.Append("续借失败书籍:"); 9 foreach (var barcode in barcodes) 10 { 11 Book reBorrowBook = _lendingInfoDbContext.Books.FirstOrDefault(b => b.BarCode == barcode); 12 if (reBorrowBook != null) 13 { 14 if (reBorrowBook.State == BookState.Borrowed && DateTime.Now-reBorrowBook.MatureTime?.Date<=TimeSpan.FromDays(3)) 15 { 16 reBorrowBook.State = BookState.ReBorrowed; 17 reBorrowBook.BorrowTime = DateTime.Now.Date; 18 reBorrowBook.MatureTime = DateTime.Now.Date+TimeSpan.FromDays(28); 19 borrowSuccess.Append($"《{reBorrowBook.Name}》、"); 20 } 21 else 22 { 23 borrowFail.Append($"《{reBorrowBook.Name}》、"); 24 } 25 } 26 } 27 borrowSuccess.AppendLine(borrowFail.ToString()); 28 await _lendingInfoDbContext.SaveChangesAsync(); 29 TempData["message"] = borrowSuccess.ToString(); 30 return RedirectToAction("PersonalInfo"); 31 }
结果:
七、查询书籍
修改之前的 Search 方法使其通过当前用户的身份返回不同页面,以及在 _LendingInfoLayout 中添加搜索框部分:
19 行通过短路使未授权用户不用登录。
1 public async Task<IActionResult> Search(string keyWord, string value) 2 { 3 BookDetails bookDetails = new BookDetails(); 4 switch (keyWord) 5 { 6 case "Name": 7 bookDetails = await _lendingInfoDbContext.BooksDetail.AsNoTracking().FirstOrDefaultAsync(b => b.Name == value); 8 break; 9 case "ISBN": 10 bookDetails = await _lendingInfoDbContext.BooksDetail.AsNoTracking().FirstOrDefaultAsync(b => b.ISBN == value); 11 break; 12 case "FetchBookNumber": 13 bookDetails = await _lendingInfoDbContext.BooksDetail.AsNoTracking().FirstOrDefaultAsync(b => b.FetchBookNumber == value); 14 break; 15 } 16 17 if (bookDetails != null) 18 { 19 if (User.Identity.IsAuthenticated&& User.IsInRole("Admin")) 20 { 21 return RedirectToAction("EditBookDetails", new { isbn = bookDetails.ISBN }); 22 } 23 else 24 { 25 return RedirectToAction("Detail", new {isbn = bookDetails.ISBN}); 26 } 27 } 28 29 TempData["message"] = "找不到该书籍"; 30 return RedirectToAction("BookDetails"); 31 }
结果: