.NET MVC中实现后台商品列表功能
详细讲解在.NET MVC中实现后台商品列表的增删改查和图片上传功能。
📋 功能概览
功能模块 | 主要职责 | 关键技术 |
---|---|---|
商品模型 | 定义商品数据结构 | Entity Framework, Data Annotations |
商品控制器 | 处理CRUD操作和图片上传 | MVC Controller, HttpPost |
列表视图 | 显示商品表格 | Razor语法, HTML Helpers |
表单视图 | 创建/编辑商品 | Bootstrap表单, 文件上传 |
图片处理 | 文件上传和存储 | HttpPostedFileBase, 路径处理 |
🗂️ 数据模型设计
商品模型 (Product.cs)
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema;public class Product {public int ProductID { get; set; }[Required(ErrorMessage = "商品名称不能为空")][StringLength(100, ErrorMessage = "商品名称不能超过100个字符")][Display(Name = "商品名称")]public string Name { get; set; }[Display(Name = "商品描述")][DataType(DataType.MultilineText)]public string Description { get; set; }[Required(ErrorMessage = "价格不能为空")][Range(0.01, 10000, ErrorMessage = "价格必须在0.01到10000之间")][Display(Name = "价格")]public decimal Price { get; set; }[Display(Name = "库存数量")]public int StockQuantity { get; set; }[Display(Name = "是否上架")]public bool IsActive { get; set; } = true;[Display(Name = "创建时间")]public DateTime CreateTime { get; set; } = DateTime.Now;[Display(Name = "图片路径")]public string ImagePath { get; set; }// 分类外键(可选)[Display(Name = "商品分类")]public int? CategoryID { get; set; }public virtual Category Category { get; set; }[NotMapped][Display(Name = "商品图片")]public HttpPostedFileBase ImageFile { get; set; } }// 商品分类模型(可选) public class Category {public int CategoryID { get; set; }[Required][StringLength(50)][Display(Name = "分类名称")]public string CategoryName { get; set; }public virtual ICollection<Product> Products { get; set; } }
数据库上下文 (ApplicationDbContext.cs)
using System.Data.Entity;public class ApplicationDbContext : DbContext {public ApplicationDbContext() : base("DefaultConnection"){}public DbSet<Product> Products { get; set; }public DbSet<Category> Categories { get; set; }protected override void OnModelCreating(DbModelBuilder modelBuilder){// 配置关系modelBuilder.Entity<Product>().HasOptional(p => p.Category).WithMany(c => c.Products).HasForeignKey(p => p.CategoryID);} }
⚙️ 商品控制器实现
ProductsController.cs
using System.Data.Entity; using System.IO; using System.Linq; using System.Web; using System.Web.Mvc;public class ProductsController : Controller {private ApplicationDbContext db = new ApplicationDbContext();// GET: 商品列表public ActionResult Index(string searchString, string sortOrder, int? categoryId){ViewBag.NameSortParam = string.IsNullOrEmpty(sortOrder) ? "name_desc" : "";ViewBag.PriceSortParam = sortOrder == "price" ? "price_desc" : "price";var products = db.Products.Include(p => p.Category);// 搜索过滤if (!string.IsNullOrEmpty(searchString)){products = products.Where(p => p.Name.Contains(searchString) || p.Description.Contains(searchString));}// 分类过滤if (categoryId.HasValue){products = products.Where(p => p.CategoryID == categoryId);}// 排序switch (sortOrder){case "name_desc":products = products.OrderByDescending(p => p.Name);break;case "price":products = products.OrderBy(p => p.Price);break;case "price_desc":products = products.OrderByDescending(p => p.Price);break;default:products = products.OrderBy(p => p.Name);break;}// 分类下拉列表数据ViewBag.CategoryID = new SelectList(db.Categories, "CategoryID", "CategoryName");return View(products.ToList());}// GET: 商品详情public ActionResult Details(int? id){if (id == null){return new HttpStatusCodeResult(HttpStatusCode.BadRequest);}Product product = db.Products.Include(p => p.Category).FirstOrDefault(p => p.ProductID == id);if (product == null){return HttpNotFound();}return View(product);}// GET: 创建商品public ActionResult Create(){ViewBag.CategoryID = new SelectList(db.Categories, "CategoryID", "CategoryName");return View();}// POST: 创建商品[HttpPost][ValidateAntiForgeryToken]public ActionResult Create(Product product){if (ModelState.IsValid){// 处理图片上传if (product.ImageFile != null && product.ImageFile.ContentLength > 0){product.ImagePath = SaveImage(product.ImageFile);}db.Products.Add(product);db.SaveChanges();TempData["SuccessMessage"] = "商品创建成功!";return RedirectToAction("Index");}ViewBag.CategoryID = new SelectList(db.Categories, "CategoryID", "CategoryName", product.CategoryID);return View(product);}// GET: 编辑商品public ActionResult Edit(int? id){if (id == null){return new HttpStatusCodeResult(HttpStatusCode.BadRequest);}Product product = db.Products.Find(id);if (product == null){return HttpNotFound();}ViewBag.CategoryID = new SelectList(db.Categories, "CategoryID", "CategoryName", product.CategoryID);return View(product);}// POST: 编辑商品[HttpPost][ValidateAntiForgeryToken]public ActionResult Edit(Product product){if (ModelState.IsValid){var existingProduct = db.Products.Find(product.ProductID);if (existingProduct == null){return HttpNotFound();}// 处理图片上传if (product.ImageFile != null && product.ImageFile.ContentLength > 0){// 删除旧图片if (!string.IsNullOrEmpty(existingProduct.ImagePath)){DeleteImage(existingProduct.ImagePath);}existingProduct.ImagePath = SaveImage(product.ImageFile);}// 更新其他字段existingProduct.Name = product.Name;existingProduct.Description = product.Description;existingProduct.Price = product.Price;existingProduct.StockQuantity = product.StockQuantity;existingProduct.IsActive = product.IsActive;existingProduct.CategoryID = product.CategoryID;db.Entry(existingProduct).State = EntityState.Modified;db.SaveChanges();TempData["SuccessMessage"] = "商品更新成功!";return RedirectToAction("Index");}ViewBag.CategoryID = new SelectList(db.Categories, "CategoryID", "CategoryName", product.CategoryID);return View(product);}// GET: 删除商品public ActionResult Delete(int? id){if (id == null){return new HttpStatusCodeResult(HttpStatusCode.BadRequest);}Product product = db.Products.Include(p => p.Category).FirstOrDefault(p => p.ProductID == id);if (product == null){return HttpNotFound();}return View(product);}// POST: 删除商品[HttpPost, ActionName("Delete")][ValidateAntiForgeryToken]public ActionResult DeleteConfirmed(int id){Product product = db.Products.Find(id);if (product == null){return HttpNotFound();}// 删除图片文件if (!string.IsNullOrEmpty(product.ImagePath)){DeleteImage(product.ImagePath);}db.Products.Remove(product);db.SaveChanges();TempData["SuccessMessage"] = "商品删除成功!";return RedirectToAction("Index");}// 图片保存方法private string SaveImage(HttpPostedFileBase imageFile){try{// 验证文件类型string[] allowedExtensions = { ".jpg", ".jpeg", ".png", ".gif" };string fileExtension = Path.GetExtension(imageFile.FileName).ToLower();if (!allowedExtensions.Contains(fileExtension)){throw new Exception("只支持 JPG, JPEG, PNG, GIF 格式的图片");}// 验证文件大小(最大2MB)if (imageFile.ContentLength > 2 * 1024 * 1024){throw new Exception("图片大小不能超过2MB");}// 生成唯一文件名string fileName = Guid.NewGuid().ToString() + fileExtension;string virtualPath = "~/Content/ProductImages/" + fileName;string physicalPath = Server.MapPath(virtualPath);// 确保目录存在string directory = Path.GetDirectoryName(physicalPath);if (!Directory.Exists(directory)){Directory.CreateDirectory(directory);}// 保存文件imageFile.SaveAs(physicalPath);return virtualPath;}catch (Exception ex){ModelState.AddModelError("ImageFile", "图片上传失败: " + ex.Message);return null;}}// 删除图片方法private void DeleteImage(string imagePath){try{string physicalPath = Server.MapPath(imagePath);if (System.IO.File.Exists(physicalPath)){System.IO.File.Delete(physicalPath);}}catch (Exception ex){// 记录日志,但不中断操作System.Diagnostics.Debug.WriteLine("删除图片失败: " + ex.Message);}}protected override void Dispose(bool disposing){if (disposing){db.Dispose();}base.Dispose(disposing);} }
🖥️ 视图实现
Index.cshtml (商品列表)
@model IEnumerable<Product>@{ViewBag.Title = "商品管理"; }<h2>商品管理</h2><div class="well">@using (Html.BeginForm("Index", "Products", FormMethod.Get, new { @class = "form-inline" })){<div class="form-group"><input type="text" name="searchString" class="form-control" placeholder="搜索商品..." value="@ViewBag.SearchString" /></div><div class="form-group">@Html.DropDownList("categoryId", ViewBag.CategoryID as SelectList, "所有分类", new { @class = "form-control" })</div><button type="submit" class="btn btn-primary">搜索</button>@Html.ActionLink("重置", "Index", null, new { @class = "btn btn-default" })} </div><p>@Html.ActionLink("创建新商品", "Create", null, new { @class = "btn btn-success" }) </p>@if (TempData["SuccessMessage"] != null) {<div class="alert alert-success">@TempData["SuccessMessage"]</div> }<table class="table table-striped table-bordered"><thead><tr><th>图片</th><th>@Html.ActionLink("商品名称", "Index", new { sortOrder = ViewBag.NameSortParam, searchString = ViewBag.SearchString })</th><th>描述</th><th>@Html.ActionLink("价格", "Index", new { sortOrder = ViewBag.PriceSortParam, searchString = ViewBag.SearchString })</th><th>库存</th><th>状态</th><th>操作</th></tr></thead><tbody>@foreach (var item in Model){<tr><td>@if (!string.IsNullOrEmpty(item.ImagePath)){<img src="@Url.Content(item.ImagePath)" alt="@item.Name" style="max-width: 60px; max-height: 60px;" class="img-thumbnail" />}else{<span class="text-muted">无图片</span>}</td><td>@Html.DisplayFor(modelItem => item.Name)</td><td>@Html.DisplayFor(modelItem => item.Description)</td><td>@Html.DisplayFor(modelItem => item.Price)</td><td>@Html.DisplayFor(modelItem => item.StockQuantity)</td><td>@if (item.IsActive){<span class="label label-success">上架</span>}else{<span class="label label-danger">下架</span>}</td><td><div class="btn-group">@Html.ActionLink("详情", "Details", new { id = item.ProductID }, new { @class = "btn btn-xs btn-info" })@Html.ActionLink("编辑", "Edit", new { id = item.ProductID }, new { @class = "btn btn-xs btn-warning" })@Html.ActionLink("删除", "Delete", new { id = item.ProductID }, new { @class = "btn btn-xs btn-danger" })</div></td></tr>}</tbody> </table>
Create.cshtml (创建商品)
@model Product@{ViewBag.Title = "创建商品"; }<h2>创建商品</h2>@using (Html.BeginForm("Create", "Products", FormMethod.Post, new { enctype = "multipart/form-data" })) {@Html.AntiForgeryToken()<div class="form-horizontal"><hr />@Html.ValidationSummary(true, "", new { @class = "text-danger" })<div class="form-group">@Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })<div class="col-md-10">@Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })@Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })</div></div><div class="form-group">@Html.LabelFor(model => model.Description, htmlAttributes: new { @class = "control-label col-md-2" })<div class="col-md-10">@Html.TextAreaFor(model => model.Description, new { @class = "form-control", rows = 4 })@Html.ValidationMessageFor(model => model.Description, "", new { @class = "text-danger" })</div></div><div class="form-group">@Html.LabelFor(model => model.Price, htmlAttributes: new { @class = "control-label col-md-2" })<div class="col-md-10">@Html.EditorFor(model => model.Price, new { htmlAttributes = new { @class = "form-control" } })@Html.ValidationMessageFor(model => model.Price, "", new { @class = "text-danger" })</div></div><div class="form-group">@Html.LabelFor(model => model.StockQuantity, htmlAttributes: new { @class = "control-label col-md-2" })<div class="col-md-10">@Html.EditorFor(model => model.StockQuantity, new { htmlAttributes = new { @class = "form-control" } })@Html.ValidationMessageFor(model => model.StockQuantity, "", new { @class = "text-danger" })</div></div><div class="form-group">@Html.LabelFor(model => model.CategoryID, "商品分类", htmlAttributes: new { @class = "control-label col-md-2" })<div class="col-md-10">@Html.DropDownList("CategoryID", null, "请选择分类", new { @class = "form-control" })@Html.ValidationMessageFor(model => model.CategoryID, "", new { @class = "text-danger" })</div></div><div class="form-group">@Html.LabelFor(model => model.ImageFile, htmlAttributes: new { @class = "control-label col-md-2" })<div class="col-md-10">@Html.TextBoxFor(model => model.ImageFile, new { type = "file", @class = "form-control" })@Html.ValidationMessageFor(model => model.ImageFile, "", new { @class = "text-danger" })<span class="help-block">支持 JPG, PNG, GIF 格式,最大 2MB</span></div></div><div class="form-group">@Html.LabelFor(model => model.IsActive, htmlAttributes: new { @class = "control-label col-md-2" })<div class="col-md-10"><div class="checkbox">@Html.EditorFor(model => model.IsActive)@Html.ValidationMessageFor(model => model.IsActive, "", new { @class = "text-danger" })</div></div></div><div class="form-group"><div class="col-md-offset-2 col-md-10"><input type="submit" value="创建" class="btn btn-primary" />@Html.ActionLink("返回列表", "Index", null, new { @class = "btn btn-default" })</div></div></div> }@section Scripts {@Scripts.Render("~/bundles/jqueryval") }
Edit.cshtml (编辑商品)
@model Product@{ViewBag.Title = "编辑商品"; }<h2>编辑商品</h2>@using (Html.BeginForm("Edit", "Products", FormMethod.Post, new { enctype = "multipart/form-data" })) {@Html.AntiForgeryToken()@Html.HiddenFor(model => model.ProductID)<div class="form-horizontal"><hr />@Html.ValidationSummary(true, "", new { @class = "text-danger" })<!-- 表单字段与Create视图类似,这里省略重复部分 --><div class="form-group">@Html.LabelFor(model => model.ImageFile, htmlAttributes: new { @class = "control-label col-md-2" })<div class="col-md-10"><!-- 显示现有图片 -->@if (!string.IsNullOrEmpty(Model.ImagePath)){<div class="current-image"><img src="@Url.Content(Model.ImagePath)" alt="当前图片" class="img-thumbnail" style="max-width: 200px;" /><br /><small>当前图片</small></div>}@Html.TextBoxFor(model => model.ImageFile, new { type = "file", @class = "form-control" })@Html.ValidationMessageFor(model => model.ImageFile, "", new { @class = "text-danger" })<span class="help-block">如要更换图片,请选择新图片</span></div></div><div class="form-group"><div class="col-md-offset-2 col-md-10"><input type="submit" value="保存" class="btn btn-primary" />@Html.ActionLink("返回列表", "Index", null, new { @class = "btn btn-default" })</div></div></div> }@section Scripts {@Scripts.Render("~/bundles/jqueryval") }
🔧 路由配置
RouteConfig.cs
public class RouteConfig {public static void RegisterRoutes(RouteCollection routes){routes.IgnoreRoute("{resource}.axd/{*pathInfo}");routes.MapRoute(name: "Default",url: "{controller}/{action}/{id}",defaults: new { controller = "Products", action = "Index", id = UrlParameter.Optional });} }
📝 数据库迁移
在程序包管理器控制台中执行:
# 启用迁移 Enable-Migrations# 添加迁移 Add-Migration InitialCreate# 更新数据库 Update-Database
💡 高级功能扩展
1. 分页功能
安装PagedList.Mvc NuGet包:
// 在控制器中添加分页 public ActionResult Index(string searchString, string sortOrder, int? categoryId, int? page) {var pageNumber = page ?? 1;var pageSize = 10;var products = // ... 查询逻辑return View(products.ToPagedList(pageNumber, pageSize)); }
2. 图片缩略图生成
private string SaveImageWithThumbnail(HttpPostedFileBase imageFile) {// 保存原图string originalPath = SaveImage(imageFile);// 生成缩略图using (var image = System.Drawing.Image.FromStream(imageFile.InputStream)){var thumbWidth = 200;var thumbHeight = (int)(image.Height * ((double)thumbWidth / image.Width));using (var thumb = new Bitmap(thumbWidth, thumbHeight))using (var graphic = Graphics.FromImage(thumb)){graphic.DrawImage(image, 0, 0, thumbWidth, thumbHeight);string thumbFileName = "thumb_" + Path.GetFileName(originalPath);string thumbPath = Server.MapPath("~/Content/ProductImages/Thumbs/" + thumbFileName);thumb.Save(thumbPath, ImageFormat.Jpeg);}}return originalPath; }
🚀 部署注意事项
图片存储:生产环境中考虑使用云存储(如Azure Blob Storage、AWS S3)
安全性:添加权限验证,确保只有管理员可以访问后台
性能优化:实现图片缓存、数据库查询优化
错误处理:添加全局异常处理
这个完整的实现提供了商品管理的所有基本功能,你可以根据具体需求进行调整和扩展。