
详细讲解在.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)
-
安全性:添加权限验证,确保只有管理员可以访问后台
-
性能优化:实现图片缓存、数据库查询优化
-
错误处理:添加全局异常处理
这个完整的实现提供了商品管理的所有基本功能,你可以根据具体需求进行调整和扩展。