.NET MVC实现商品列表后台管理

理解需求与架构设计

在.NET MVC中实现后台商品列表功能,需明确核心需求:商品数据的增删改查(CRUD)、分页、搜索、排序及权限控制。采用三层架构(表现层、业务逻辑层、数据访问层)分离关注点,确保代码可维护性。

表现层由MVC的Controller和View构成,业务逻辑层处理核心规则,数据访问层通过Entity Framework Core或Dapper与数据库交互。前端可采用Razor视图或搭配AJAX实现动态加载。

数据库设计与模型定义

商品表(Products)基础字段应包括:

CREATE TABLE Products (
    Id INT PRIMARY KEY IDENTITY,
    Name NVARCHAR(100) NOT NULL,
    Price DECIMAL(18,2) NOT NULL,
    Description NVARCHAR(MAX),
    Stock INT DEFAULT 0,
    CreatedAt DATETIME DEFAULT GETDATE(),
    CategoryId INT FOREIGN KEY REFERENCES Categories(Id)
);

对应C#模型类:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Description { get; set; }
    public int Stock { get; set; }
    public DateTime CreatedAt { get; set; }
    public int CategoryId { get; set; }
    public Category Category { get; set; }
}

实现数据访问层

使用Entity Framework Core的DbContext进行数据操作:

public class AppDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>()
            .HasOne(p => p.Category)
            .WithMany()
            .HasForeignKey(p => p.CategoryId);
    }
}

通用仓储模式封装CRUD:

public interface IRepository<T> where T : class
{
    IQueryable<T> GetAll();
    Task<T> GetByIdAsync(int id);
    Task AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(int id);
}

业务逻辑层实现

商品服务类处理分页与过滤逻辑:

public class ProductService
{
    private readonly IRepository<Product> _repository;

    public ProductService(IRepository<Product> repository)
    {
        _repository = repository;
    }

    public async Task<PagedResult<Product>> GetPagedProductsAsync(int page, int pageSize, string searchTerm)
    {
        var query = _repository.GetAll();
        
        if (!string.IsNullOrEmpty(searchTerm))
        {
            query = query.Where(p => p.Name.Contains(searchTerm));
        }

        return await query.OrderBy(p => p.Name)
                         .GetPagedResultAsync(page, pageSize);
    }
}

分页扩展方法:

public static async Task<PagedResult<T>> GetPagedResultAsync<T>(this IQueryable<T> query, int page, int pageSize)
{
    var result = new PagedResult<T>
    {
        CurrentPage = page,
        PageSize = pageSize,
        RowCount = await query.CountAsync()
    };

    var pageCount = (double)result.RowCount / pageSize;
    result.PageCount = (int)Math.Ceiling(pageCount);

    var skip = (page - 1) * pageSize;    
    result.Results = await query.Skip(skip).Take(pageSize).ToListAsync();

    return result;
}

控制器与视图实现

AdminController处理商品列表请求:

[Authorize(Roles = "Admin")]
public class AdminController : Controller
{
    private readonly ProductService _productService;

    public AdminController(ProductService productService)
    {
        _productService = productService;
    }

    public async Task<IActionResult> ProductList(int page = 1, string search = "")
    {
        var model = await _productService.GetPagedProductsAsync(page, 10, search);
        return View(model);
    }
}

Razor视图(ProductList.cshtml):

@model PagedResult<Product>

<table class="table">
    <thead>
        <tr>
            <th>ID</th>
            <th>@Html.ActionLink("Name", "ProductList", new { sort = "name" })</th>
            <th>Price</th>
            <th>Stock</th>
            <th>Actions</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Results)
        {
            <tr>
                <td>@item.Id</td>
                <td>@item.Name</td>
                <td>@item.Price.ToString("C")</td>
                <td>@item.Stock</td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

<div>
    @if (Model.PageCount > 1)
    {
        for (int p = 1; p <= Model.PageCount; p++)
        {
            <a href="@Url.Action("ProductList", new { page = p })" 
               class="@(p == Model.CurrentPage ? "active" : "")">@p</a>
        }
    }
</div>

前端优化与AJAX加载

使用jQuery实现无刷新分页:

$(document).ready(function() {
    $('#searchForm').submit(function(e) {
        e.preventDefault();
        loadProducts(1);
    });

    $(document).on('click', '.pager a', function() {
        var page = $(this).data('page');
        loadProducts(page);
        return false;
    });
});

function loadProducts(page) {
    var search = $('#searchTerm').val();
    $.get(**********("ProductListPartial")', { page: page, search: search })
        .done(function(data) {
            $('#productTable').html(data);
        });
}

部分视图(_ProductListPartial.cshtml):

@model PagedResult<Product>

<table id="productTable">
    <!-- 动态内容由AJAX填充 -->
</table>

安全与性能优化

添加防CSRF攻击保护:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id)
{
    await _productService.DeleteAsync(id);
    return RedirectToAction("ProductList");
}

视图中的Token生成:

<form asp-action="Delete">
    @Html.AntiForgeryToken()
    <input type="hidden" name="id" value="@item.Id" />
    <button type="submit">Delete</button>
</form>

缓存常用查询结果:

[ResponseCache(Duration = 60)]
public async Task<IActionResult> ProductList()
{
    // ...
}

单元测试示例

测试商品分页逻辑:

[Fact]
public async Task GetPagedProducts_ReturnsCorrectPage()
{
    var mockRepo = new Mock<IRepository<Product>>();
    mockRepo.Setup(repo => repo.GetAll())
            .Returns(TestProducts().AsQueryable());

    var service = new ProductService(mockRepo.Object);
    var result = await service.GetPagedProductsAsync(2, 3, "");

    Assert.Equal(3, result.Results.Count);
    Assert.Equal("Product4", result.Results.First().Name);
}

private List<Product> TestProducts()
{
    return new List<Product>
    {
        new Product { Id = 1, Name = "Product1" },
        new Product { Id = 2, Name = "Product2" },
        // ... 共10个测试商品
    };
}

BbS.okapop123.sbs/PoSt/1122_120535.HtM
BbS.okapop124.sbs/PoSt/1122_772426.HtM
BbS.okapop125.sbs/PoSt/1122_460675.HtM
BbS.okapop126.sbs/PoSt/1122_444812.HtM
BbS.okapop127.sbs/PoSt/1122_629560.HtM
BbS.okapop128.sbs/PoSt/1122_342526.HtM
BbS.okapop129.sbs/PoSt/1122_340089.HtM
BbS.okapop130.sbs/PoSt/1122_060685.HtM
BbS.okapop131.sbs/PoSt/1122_697281.HtM
BbS.okapop132.sbs/PoSt/1122_023045.HtM
BbS.okapop123.sbs/PoSt/1122_682470.HtM
BbS.okapop124.sbs/PoSt/1122_127011.HtM
BbS.okapop125.sbs/PoSt/1122_865776.HtM
BbS.okapop126.sbs/PoSt/1122_036249.HtM
BbS.okapop127.sbs/PoSt/1122_723496.HtM
BbS.okapop128.sbs/PoSt/1122_009175.HtM
BbS.okapop129.sbs/PoSt/1122_563198.HtM
BbS.okapop130.sbs/PoSt/1122_033183.HtM
BbS.okapop131.sbs/PoSt/1122_194913.HtM
BbS.okapop132.sbs/PoSt/1122_434751.HtM
BbS.okapop133.sbs/PoSt/1122_970604.HtM
BbS.okapop134.sbs/PoSt/1122_368795.HtM
BbS.okapop135.sbs/PoSt/1122_578698.HtM
BbS.okapop136.sbs/PoSt/1122_956611.HtM
BbS.okapop137.sbs/PoSt/1122_239457.HtM
BbS.okapop138.sbs/PoSt/1122_309598.HtM
BbS.okapop139.sbs/PoSt/1122_333941.HtM
BbS.okapop140.sbs/PoSt/1122_809368.HtM
BbS.okapop141.sbs/PoSt/1122_600736.HtM
BbS.okapop142.sbs/PoSt/1122_226563.HtM
BbS.okapop133.sbs/PoSt/1122_567553.HtM
BbS.okapop134.sbs/PoSt/1122_502197.HtM
BbS.okapop135.sbs/PoSt/1122_794653.HtM
BbS.okapop136.sbs/PoSt/1122_559051.HtM
BbS.okapop137.sbs/PoSt/1122_765731.HtM
BbS.okapop138.sbs/PoSt/1122_032726.HtM
BbS.okapop139.sbs/PoSt/1122_453652.HtM
BbS.okapop140.sbs/PoSt/1122_544046.HtM
BbS.okapop141.sbs/PoSt/1122_474906.HtM
BbS.okapop142.sbs/PoSt/1122_978510.HtM
BbS.okapop133.sbs/PoSt/1122_927827.HtM
BbS.okapop134.sbs/PoSt/1122_319438.HtM
BbS.okapop135.sbs/PoSt/1122_994620.HtM
BbS.okapop136.sbs/PoSt/1122_117236.HtM
BbS.okapop137.sbs/PoSt/1122_797163.HtM
BbS.okapop138.sbs/PoSt/1122_996465.HtM
BbS.okapop139.sbs/PoSt/1122_888221.HtM
BbS.okapop140.sbs/PoSt/1122_660554.HtM
BbS.okapop141.sbs/PoSt/1122_153540.HtM
BbS.okapop142.sbs/PoSt/1122_801535.HtM
BbS.okapop133.sbs/PoSt/1122_813113.HtM
BbS.okapop134.sbs/PoSt/1122_322901.HtM
BbS.okapop135.sbs/PoSt/1122_555645.HtM
BbS.okapop136.sbs/PoSt/1122_123485.HtM
BbS.okapop137.sbs/PoSt/1122_128397.HtM
BbS.okapop138.sbs/PoSt/1122_072848.HtM
BbS.okapop139.sbs/PoSt/1122_983972.HtM
BbS.okapop140.sbs/PoSt/1122_222128.HtM
BbS.okapop141.sbs/PoSt/1122_345389.HtM
BbS.okapop142.sbs/PoSt/1122_595924.HtM
BbS.okapop133.sbs/PoSt/1122_513505.HtM
BbS.okapop134.sbs/PoSt/1122_194254.HtM
BbS.okapop135.sbs/PoSt/1122_304253.HtM
BbS.okapop136.sbs/PoSt/1122_459819.HtM
BbS.okapop137.sbs/PoSt/1122_553760.HtM
BbS.okapop138.sbs/PoSt/1122_637049.HtM
BbS.okapop139.sbs/PoSt/1122_274027.HtM
BbS.okapop140.sbs/PoSt/1122_136115.HtM
BbS.okapop141.sbs/PoSt/1122_443756.HtM
BbS.okapop142.sbs/PoSt/1122_170297.HtM
BbS.okapop133.sbs/PoSt/1122_025371.HtM
BbS.okapop134.sbs/PoSt/1122_203734.HtM
BbS.okapop135.sbs/PoSt/1122_994650.HtM
BbS.okapop136.sbs/PoSt/1122_157609.HtM
BbS.okapop137.sbs/PoSt/1122_226936.HtM
BbS.okapop138.sbs/PoSt/1122_844703.HtM
BbS.okapop139.sbs/PoSt/1122_385002.HtM
BbS.okapop140.sbs/PoSt/1122_570323.HtM
BbS.okapop141.sbs/PoSt/1122_807878.HtM
BbS.okapop142.sbs/PoSt/1122_089633.HtM

#牛客AI配图神器#

全部评论

相关推荐

迷茫的大四🐶:💐孝子启动失败,改为启动咏鹅
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务