.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;
}

🚀 部署注意事项

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

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

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

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

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

相关推荐
学IT的周星星2 天前
《Spring MVC奇幻漂流记:当Java遇上Web的奇妙冒险》
java·spring·mvc
老葱头蒸鸡3 天前
(23)ASP.NET Core2.2 EF关系数据库建模
后端·asp.net
Elieal3 天前
SpringMVC 入门:核心概念与第一个 HelloWorld 案例
mvc·maven
这周也會开心3 天前
Spring-MVC响应
java·spring·mvc
老葱头蒸鸡3 天前
(14)ASP.NET Core2.2 中的日志记录
后端·asp.net
William_cl4 天前
【连载7】 C# MVC 跨框架异常处理对比:.NET Framework 与 .NET Core 实现差异
c#·mvc·.net
准时准点睡觉4 天前
HTTP 错误 403.14 - Forbidden Web 服务器被配置为不列出此目录的内容——错误代码:0x00000000
运维·服务器·iis·asp.net
城管不管5 天前
Spring + Spring MVC + MyBatis
java·spring·mvc
老葱头蒸鸡5 天前
(8)ASP.NET Core2.2 中的MVC路由一
后端·asp.net·mvc