.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 Products { get; set; }
}

数据库上下文 (ApplicationDbContext.cs)

using System.Data.Entity;

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext() : base("DefaultConnection")
    {
    }
    
    public DbSet Products { get; set; }
    public DbSet Categories { get; set; }
    
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // 配置关系
        modelBuilder.Entity()
            .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

@{
    ViewBag.Title = "商品管理";
}

商品管理

@using (Html.BeginForm("Index", "Products", FormMethod.Get, new { @class = "form-inline" })) {
@Html.DropDownList("categoryId", ViewBag.CategoryID as SelectList, "所有分类", new { @class = "form-control" })
@Html.ActionLink("重置", "Index", null, new { @class = "btn btn-default" }) }

@Html.ActionLink("创建新商品", "Create", null, new { @class = "btn btn-success" })

@if (TempData["SuccessMessage"] != null) {
@TempData["SuccessMessage"]
} @foreach (var item in Model) { }
图片 @Html.ActionLink("商品名称", "Index", new { sortOrder = ViewBag.NameSortParam, searchString = ViewBag.SearchString }) 描述 @Html.ActionLink("价格", "Index", new { sortOrder = ViewBag.PriceSortParam, searchString = ViewBag.SearchString }) 库存 状态 操作
@if (!string.IsNullOrEmpty(item.ImagePath)) { @item.Name } else { 无图片 } @Html.DisplayFor(modelItem => item.Name) @Html.DisplayFor(modelItem => item.Description) @Html.DisplayFor(modelItem => item.Price) @Html.DisplayFor(modelItem => item.StockQuantity) @if (item.IsActive) { 上架 } else { 下架 }
@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" })

Create.cshtml (创建商品)

@model Product

@{
    ViewBag.Title = "创建商品";
}

创建商品

@using (Html.BeginForm("Create", "Products", FormMethod.Post, new { enctype = "multipart/form-data" })) { @Html.AntiForgeryToken()

@Html.ValidationSummary(true, "", new { @class = "text-danger" })
@Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
@Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
@Html.LabelFor(model => model.Description, htmlAttributes: new { @class = "control-label col-md-2" })
@Html.TextAreaFor(model => model.Description, new { @class = "form-control", rows = 4 }) @Html.ValidationMessageFor(model => model.Description, "", new { @class = "text-danger" })
@Html.LabelFor(model => model.Price, htmlAttributes: new { @class = "control-label col-md-2" })
@Html.EditorFor(model => model.Price, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Price, "", new { @class = "text-danger" })
@Html.LabelFor(model => model.StockQuantity, htmlAttributes: new { @class = "control-label col-md-2" })
@Html.EditorFor(model => model.StockQuantity, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.StockQuantity, "", new { @class = "text-danger" })
@Html.LabelFor(model => model.CategoryID, "商品分类", htmlAttributes: new { @class = "control-label col-md-2" })
@Html.DropDownList("CategoryID", null, "请选择分类", new { @class = "form-control" }) @Html.ValidationMessageFor(model => model.CategoryID, "", new { @class = "text-danger" })
@Html.LabelFor(model => model.ImageFile, htmlAttributes: new { @class = "control-label col-md-2" })
@Html.TextBoxFor(model => model.ImageFile, new { type = "file", @class = "form-control" }) @Html.ValidationMessageFor(model => model.ImageFile, "", new { @class = "text-danger" }) 支持 JPG, PNG, GIF 格式,最大 2MB
@Html.LabelFor(model => model.IsActive, htmlAttributes: new { @class = "control-label col-md-2" })
@Html.EditorFor(model => model.IsActive) @Html.ValidationMessageFor(model => model.IsActive, "", new { @class = "text-danger" })
@Html.ActionLink("返回列表", "Index", null, new { @class = "btn btn-default" })
} @section Scripts { @Scripts.Render("~/bundles/jqueryval") }

Edit.cshtml (编辑商品)

@model Product

@{
    ViewBag.Title = "编辑商品";
}

编辑商品

@using (Html.BeginForm("Edit", "Products", FormMethod.Post, new { enctype = "multipart/form-data" })) { @Html.AntiForgeryToken() @Html.HiddenFor(model => model.ProductID)

@Html.ValidationSummary(true, "", new { @class = "text-danger" })
@Html.LabelFor(model => model.ImageFile, htmlAttributes: new { @class = "control-label col-md-2" })
@if (!string.IsNullOrEmpty(Model.ImagePath)) {
当前图片
当前图片
} @Html.TextBoxFor(model => model.ImageFile, new { type = "file", @class = "form-control" }) @Html.ValidationMessageFor(model => model.ImageFile, "", new { @class = "text-danger" }) 如要更换图片,请选择新图片
@Html.ActionLink("返回列表", "Index", null, new { @class = "btn btn-default" })
} @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;
}

部署注意事项

  1. 图片存储:生产环境中考虑使用云存储(如Azure Blob Storage、AWS S3)

  2. 安全性:添加权限验证,确保只有管理员可以访问后台

  3. 性能优化:实现图片缓存、数据库查询优化

  4. 错误处理:添加全局异常处理

这个完整的实现提供了商品管理的所有基本功能,你可以根据具体需求进行调整和扩展。

posted @ 2025-10-26 09:47  ycfenxi  阅读(2)  评论(0)    收藏  举报