ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(六)学生借阅/预约/查询书籍事务
前言:
本系列文章主要为我之前所学知识的一次微小的实践,以我学校图书馆管理系统为雏形所作。
本系列文章主要参考资料:
微软文档: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 的默认布局页,现在以该默认布局页为基础,添加自己所需要的信息:
@using Microsoft.AspNetCore.Http.Extensions @using Microsoft.AspNetCore.Authorization @inject IAuthorizationService AuthorizationService <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - LibraryDemo</title> <environment include="Development"> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" /> <link rel="stylesheet" href="~/css/site.css" /> </environment> <environment exclude="Development"> <link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css" asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css" asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" /> <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" /> </environment> </head> <body> <nav class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a asp-area="" asp-controller="BookInfo" asp-action="Index" class="navbar-brand">LibraryDemo</a> </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li><a asp-area="" asp-controller="BookInfo" asp-action="Index">首页</a></li> <li> @if (User.Identity.IsAuthenticated) { <a asp-controller="BookInfo" asp-action="PersonalInfo">@User.Identity.Name</a> } else { <a asp-area="" asp-controller="StudentAccount" asp-action="Login" asp-route-returnUrl="@(Context.Request.GetDisplayUrl())">登录</a> } </li> <li><a asp-area="" asp-controller="BookInfo" asp-action="Recommend">推荐图书</a></li> <li><a href="mailto:Nanase@cnblogs.com">联系我们</a></li> @if (User.Identity.IsAuthenticated) { <li> <a asp-action="Logout" asp-controller="StudentAccount" asp-route-returnUrl="@(Context.Request.GetDisplayUrl())">注销</a> </li> } </ul> </div> </div> </nav> <div align="center"> <br /> <form action="@Url.Action("Search", "BookInfo")"> @Html.DropDownList("keyword", new List<SelectListItem>() { new SelectListItem("书名", "Name"), new SelectListItem("ISBN", "ISBN"), new SelectListItem("索书号", "FetchBookNumber"), }) <input type="text" name="value"/> <button type="submit"><span class="glyphicon glyphicon-search"></span></button> </form> @if (TempData["message"] != null) { <br/> <p class="text-success">@TempData["message"]</p> <br/> } </div> <partial name="_CookieConsentPartial" /> <div class="container body-content"> @RenderBody() <hr /> </div> <div class="container" style="margin-top: 20px;"> <footer> <p>© 2018 - LibraryDemo</p> </footer> </div> <environment include="Development"> <script src="~/lib/jquery/dist/jquery.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script> <script src="~/js/site.js" asp-append-version="true"></script> </environment> <environment exclude="Development"> <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.3.1.min.js" asp-fallback-src="~/lib/jquery/dist/jquery.min.js" asp-fallback-test="window.jQuery" crossorigin="anonymous" integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT"></script> <script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js" asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js" asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal" crossorigin="anonymous" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"></script> <script src="~/js/site.min.js" asp-append-version="true"></script> </environment> @RenderSection("Scripts", required: false) </body> </html>View Code
现在大体框架:
除了默认的 RenderBody 外,可以指定特定的部分放在页面的不同地方,在布局页中使用@RenderSection("SectionName"):
@RenderSection("SectionName")
且在视图页中使用指定特定的节@section SectionName{ };
@section SectionName{ };
则该视图页中的 SectionName 部分会被提取出来放到布局页对应的位置。
二、管理员编辑借阅信息
动作方法:
在此对数据库的表格使用 Include 方法使 EF 应用其导航属性以获得 KeepingBooks 列表,否则使用 Student 对象 KeepingBooks 属性只会返回空。
[Authorize(Roles = "Admin")] public IActionResult EditLendingInfo(string barcode) { if (barcode == null) { return RedirectToAction("BookDetails"); } Book book = _lendingInfoDbContext.Books.FirstOrDefault(b => b.BarCode == barcode); return View(book); } [HttpPost] [Authorize(Roles = "Admin")] [ValidateAntiForgeryToken] public async Task<IActionResult> EditLendingInfo([Bind("BarCode,ISBN,BorrowTime,KeeperId,AppointedLatestTime,State")]Book book) { if (ModelState.IsValid) { if (book.BorrowTime > DateTime.Now) { ModelState.AddModelError("", "请检查外借时间"); return View(book); } if (book.AppointedLatestTime.HasValue) { if (book.AppointedLatestTime < DateTime.Now) { ModelState.AddModelError("", "请检查预约时间"); return View(book); } if (book.KeeperId == null) { ModelState.AddModelError("", "不存在该学生"); return View(book); } } StudentInfo student = await _lendingInfoDbContext.Students.Include(s => s.KeepingBooks).FirstOrDefaultAsync(s => s.UserName == book.KeeperId); Book addedBook = _lendingInfoDbContext.Books .Include(b => b.Keeper).ThenInclude(k => k.KeepingBooks) .FirstOrDefault(b => b.BarCode == book.BarCode); if (addedBook == null) { return RedirectToAction("Books", new { isbn = book.ISBN }); } StudentInfo preStudent = addedBook.Keeper; AppointmentOrLending targetLending = preStudent?.KeepingBooks.FirstOrDefault(b => b.BookId == addedBook.BarCode); addedBook.AppointedLatestTime = book.AppointedLatestTime; addedBook.State = book.State; addedBook.BorrowTime = book.BorrowTime; addedBook.MatureTime = null; preStudent?.KeepingBooks.Remove(targetLending); if (addedBook.BorrowTime.HasValue) { if (book.KeeperId == null) { ModelState.AddModelError("", "请检查借阅者"); return View(book); } if (student == null) { ModelState.AddModelError("", "不存在该学生"); return View(book); } if (student != null) { if (student.KeepingBooks.Count >= student.MaxBooksNumber) { TempData["message"] = "该学生借书已超过上限"; } addedBook.State = BookState.Borrowed; student.KeepingBooks.Add(new AppointmentOrLending() { BookId = addedBook.BarCode, StudentId = student.UserName }); addedBook.Keeper = student; } addedBook.MatureTime = addedBook.BorrowTime + TimeSpan.FromDays(28); } TempData["message"] = "保存成功"; await _lendingInfoDbContext.SaveChangesAsync(); return RedirectToAction("Books", new { isbn = book.ISBN }); } return View(book); }
将 BookState 枚举提取成分部视图 _BookStatePartial:
@using LibraryDemo.Models.DomainModels @model Book <div class="form-group"> @Html.LabelFor(b => b.State) @Html.DropDownListFor(b => b.State, Enum.GetValues(typeof(BookState)).Cast<Enum>().Select(state => { string enumVal = Enum.GetName(typeof(BookState), state); string displayVal; switch (enumVal) { case "Normal": displayVal = "可借阅"; break; case "Readonly": displayVal = "馆内阅览"; break; case "Borrowed": displayVal = "已借出"; break; case "ReBorrowed": displayVal = "被续借"; break; case "Appointed": displayVal = "被预约"; break; default: displayVal = ""; break; } return new SelectListItem() { Text = displayVal, Value = enumVal, Selected = Model.State.ToString() == enumVal }; })) </div>
Html.DisplayFor 方法是 ASP.NET 内置对各种属性进行展示的方法,可以在项目的 Views 文件夹中的 Shared 文件夹创建对应类型的 Editor 模板供其使用:
在此创建一个 DateTime.cshtml,于是我们使用 Html.DisplayFor 用于展示 DateTime 数据时只会显示年份/月份/天数:
@model DateTime? @Model?.ToString("yyyy/M/dd")
视图中第 40 行使用 partial TagHelper 指定其 name 为 _BookStatePartial 以应用分部视图:
@model LibraryDemo.Models.DomainModels.Book @{ ViewData["Title"] = "EditLendingInfo"; Layout="_LendingLayout"; } <h2>@Model.BarCode</h2> <h3>@Model.Name</h3> <br/> <script> window.onload = function() { $("input").addClass("form-control"); } window.onbeforeunload = function (event) { return "您的数据未保存,确定退出?"; } function removeOnbeforeunload() { window.onbeforeunload = ""; } </script> @Html.ValidationSummary(false,"",new{@class="text-danger"}) <form asp-action="EditLendingInfo" method="post"> @Html.HiddenFor(b => b.BarCode) @Html.HiddenFor(b => b.ISBN) <div class="form-group"> @Html.LabelFor(b => b.KeeperId) @Html.EditorFor(b => b.KeeperId) </div> <div class="form-group"> @Html.LabelFor(b => b.BorrowTime) @Html.EditorFor(b => b.BorrowTime) </div> <div class="form-group"> @Html.LabelFor(b => b.AppointedLatestTime) @Html.EditorFor(b => b.AppointedLatestTime) </div> <partial model="@Model" name="_BookStatePartial"/> <input type="submit" onclick="return removeOnbeforeunload()" class="btn-primary"/> </form>
结果:
三、查看个人信息
这里通过 User.Identity.Name 获取当前登录人的信息以选定当前登录的学生:
[Authorize] public async Task<IActionResult> PersonalInfo() { StudentInfo student = await _lendingInfoDbContext.Students.Include(s => s.KeepingBooks).ThenInclude(k => k.Book) .FirstOrDefaultAsync(s => s.UserName == User.Identity.Name); decimal fine = 0; foreach (var book in student.KeepingBooks.Where(b => b.Book.MatureTime < DateTime.Now && !b.AppointingDateTime.HasValue)) { fine += (DateTime.Now - book.Book.MatureTime.Value).Days * (decimal)0.2; book.Book.State = book.Book.State == BookState.Appointed ? BookState.Appointed : BookState.Expired; } student.Fine = fine; PersonalInfoViewModel model = new PersonalInfoViewModel() { Student = student, BookingBook = _lendingInfoDbContext.Books.FirstOrDefault(b => b.BarCode == student.AppointingBookBarCode) }; return View(model); }
视图:
@model LibraryDemo.Models.PersonalInfoViewModel @{ ViewData["Title"] = "PersonalInfo"; Layout = "_LendingLayout"; } <link rel="stylesheet" href="~/css/BookInfo.css" /> <script> function ensureCancel() { if (confirm("确定取消预约?")) { return true; } return false; } </script> <h2>@Model.Student.Name</h2> <br /> @if (Model.Student.KeepingBooks.Any(b => b.Book.MatureTime < DateTime.Now)) { <table> <thead> <tr> <th colspan="5">过期书籍</th> </tr> </thead> <tr> <th>书名</th> <th>条形码</th> <th>状态</th> <th>到期时间</th> <th>索书号</th> </tr> @foreach (var matureBook in Model.Student.KeepingBooks.Where(b => b.Book.MatureTime < DateTime.Now && !b.AppointingDateTime.HasValue)) { <tr> <td>@matureBook.Book.Name</td> <td>@matureBook.Book.BarCode</td> <td>@Html.DisplayFor(b => matureBook.Book.State)</td> <td>@matureBook.Book.MatureTime?.ToString("yyyy/MM/dd")</td> <td>@matureBook.Book.FetchBookNumber</td> </tr> } <tfoot><tr><td colspan="5">罚款:@Model.Student.Fine</td></tr></tfoot> </table> } <form asp-action="ReBorrow" method="post"> <table> <tr> <th>续借</th> <th>书名</th> <th>条形码</th> <th>状态</th> <th>到期时间</th> <th>索书号</th> </tr> @if (!Model.Student.KeepingBooks.Any()) { <tr> <td colspan="6" style="text-align: center">未借阅书本</td> </tr> } else { foreach (var keepingBook in Model.Student.KeepingBooks.Where(b=>!b.AppointingDateTime.HasValue)) { <tr> <td><input type="checkbox" value="@keepingBook.Book.BarCode" name="barcodes"/></td> <td>@keepingBook.Book.Name</td> <td>@keepingBook.Book.BarCode</td> <td>@Html.DisplayFor(b=>keepingBook.Book.State)</td> <td>@keepingBook.Book.MatureTime?.ToString("yyyy/MM/dd")</td> <td>@keepingBook.Book.FetchBookNumber</td> </tr> } } </table> <br/> <input type="submit" class="btn-primary btn" value="续借"/> </form> <br /> @if (Model.BookingBook != null) { <form asp-action="CancelAppointing"> <table> <tr> <th>书名</th> <th>条形码</th> <th>状态</th> <th>预约时间</th> <th>索书号</th> </tr> <book-info book="@Model.BookingBook" is-booking-book="true"></book-info> </table> <br /> <input type="hidden" name="barcode" value="@Model.BookingBook.BarCode"/> <input type="submit" value="取消预约" class="btn btn-danger" onclick="return ensureCancel()"/> </form> }
结果:
四、借阅书籍
由于暂时未有获取二维码的接口,仅通过直接访问 Lending 模拟借阅:
[Authorize] public async Task<IActionResult> Lending(string barcode) { Book targetBook=await _lendingInfoDbContext.Books.Include(b=>b.Appointments).FirstOrDefaultAsync(b => b.BarCode == barcode); if (targetBook==null) { TempData["message"] = "请重新扫描书籍"; return RedirectToAction("PersonalInfo"); } if (targetBook.Appointments.Any(a=>a.AppointingDateTime.HasValue)) { TempData["message"] = "此书已被预约"; return RedirectToAction("PersonalInfo"); } if (targetBook.State==BookState.Readonly) { TempData["message"] = "此书不供外借"; return RedirectToAction("PersonalInfo"); } targetBook.State = BookState.Borrowed; targetBook.BorrowTime = DateTime.Now.Date; targetBook.MatureTime = DateTime.Now.Date+TimeSpan.FromDays(28); StudentInfo student = await _lendingInfoDbContext.Students.Include(s=>s.KeepingBooks).FirstOrDefaultAsync(s => s.UserName == User.Identity.Name); student.KeepingBooks.Add(new AppointmentOrLending() { BookId = targetBook.BarCode, StudentId = student.UserName }); await _lendingInfoDbContext.SaveChangesAsync(); TempData["message"] = "借书成功"; return RedirectToAction("PersonalInfo"); }
结果:
六、续借书籍
动作方法:
[Authorize] [HttpPost] public async Task<IActionResult> ReBorrow(IEnumerable<string> barcodes) { StringBuilder borrowSuccess = new StringBuilder(); StringBuilder borrowFail = new StringBuilder(); borrowSuccess.Append("成功续借书籍:"); borrowFail.Append("续借失败书籍:"); foreach (var barcode in barcodes) { Book reBorrowBook = _lendingInfoDbContext.Books.FirstOrDefault(b => b.BarCode == barcode); if (reBorrowBook != null) { if (reBorrowBook.State == BookState.Borrowed && DateTime.Now-reBorrowBook.MatureTime?.Date<=TimeSpan.FromDays(3)) { reBorrowBook.State = BookState.ReBorrowed; reBorrowBook.BorrowTime = DateTime.Now.Date; reBorrowBook.MatureTime = DateTime.Now.Date+TimeSpan.FromDays(28); borrowSuccess.Append($"《{reBorrowBook.Name}》、"); } else { borrowFail.Append($"《{reBorrowBook.Name}》、"); } } } borrowSuccess.AppendLine(borrowFail.ToString()); await _lendingInfoDbContext.SaveChangesAsync(); TempData["message"] = borrowSuccess.ToString(); return RedirectToAction("PersonalInfo"); }
结果:
七、查询书籍
修改之前的 Search 方法使其通过当前用户的身份返回不同页面,以及在 _LendingInfoLayout 中添加搜索框部分:
19 行通过短路使未授权用户不用登录。
public async Task<IActionResult> Search(string keyWord, string value) { BookDetails bookDetails = new BookDetails(); switch (keyWord) { case "Name": bookDetails = await _lendingInfoDbContext.BooksDetail.AsNoTracking().FirstOrDefaultAsync(b => b.Name == value); break; case "ISBN": bookDetails = await _lendingInfoDbContext.BooksDetail.AsNoTracking().FirstOrDefaultAsync(b => b.ISBN == value); break; case "FetchBookNumber": bookDetails = await _lendingInfoDbContext.BooksDetail.AsNoTracking().FirstOrDefaultAsync(b => b.FetchBookNumber == value); break; } if (bookDetails != null) { if (User.Identity.IsAuthenticated&& User.IsInRole("Admin")) { return RedirectToAction("EditBookDetails", new { isbn = bookDetails.ISBN }); } else { return RedirectToAction("Detail", new {isbn = bookDetails.ISBN}); } } TempData["message"] = "找不到该书籍"; return RedirectToAction("BookDetails"); }
结果:
- ASP.NET Core 打造一个简单的图书馆管理系统(九) 学生信息增删(终章)
- ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(三)密码修改以及密码重置
- ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(二)用户数据库初始化、基本登录页面以及授权逻辑的建立
- ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(一) 基本模型以及数据库的建立
- ASP.NET Core 打造一个简单的图书馆管理系统(修正版)(四)外借/阅览图书信息的增删改查
- ASP.NET Core 打造一个简单的图书馆管理系统(五)初始化书籍信息
- ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(三)图书信息的增删改查
- ASP.NET Core 打造一个简单的图书馆管理系统(七)外借/阅览图书信息的增删改查
- ASP.NET Core 打造一个简单的图书馆管理系统(四)密码修改以及密码重置
- ASP.NET Core 打造一个简单的图书馆管理系统(三)基本登录页面以及授权逻辑的建立
- ASP.NET Core 打造一个简单的图书馆管理系统(二)Code First 多对多关系的建立
- 在ASP.NET Core中通过EF Core实现一个简单的全局过滤查询
- 一个简单的学生管理系统 能添加学生 查询学生
- sql server 关于表中只增标识问题 C# 实现自动化打开和关闭可执行文件(或 关闭停止与系统交互的可执行文件) ajaxfileupload插件上传图片功能,用MVC和aspx做后台各写了一个案例 将小写阿拉伯数字转换成大写的汉字, C# WinForm 中英文实现, 国际化实现的简单方法 ASP.NET Core 2 学习笔记(六)ASP.NET Core 2 学习笔记(三)
- asp.net core 身份认证/权限管理系统简介及简单案例
- ASP.NET学生信息管理系统-班级建制(一) 班级建制管理
- asp.net学生信息管理系统-参数设置简介(三)
- 推荐一个简单、轻量、功能非常强大的C#/ASP.NET定时任务执行管理器组件–FluentScheduler
- 一步一步使用Ext JS MVC与Asp.Net MVC 3开发简单的CMS后台管理系统之登录窗口调试
- java写的一个简单学生管理系统[改进]