.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
