简介:本资源是一套基于Asp.Net MVC框架与Layui前端UI实现的完整CRUD(增删改查)功能示例项目,面向Web开发初学者及.NET全栈入门者,聚焦MVC分层架构实践、前后端协同交互与基础业务功能落地。压缩包共687个文件,涵盖250个运行依赖dll、54个Layui及业务逻辑js、35个cshtml视图页、33个config配置文件、35个css样式资源,以及cs模型类、xml配置、sqlite轻量数据库等,整体大小65.44MB,结构清晰体现Model-View-Controller典型组织方式。已有494人学习下载,适合用于快速搭建本地开发环境、理解EF数据操作流程、掌握Layui表格+弹窗+表单验证的集成用法,并可直接复用AJAX请求封装、分页排序组件及基础安全处理逻辑。
1. 一个能当天跑通、第二天就能塞进真实业务的后台管理骨架:Asp.Net MVC + Layui 的 CRUD 实战模板
你有没有遇到过这种场景:接了个内部系统需求,要快速搭个带用户管理、商品列表、订单查询的后台,老板说“下周初就要能填数据”,但你翻遍 NuGet 和 GitHub,不是太重(带全套权限+工作流+审计日志),就是太轻(只有 HTML 静态页+假数据)。这个 Asp.Net MVC + Layui 的增删改查模板,就是为这种“夹缝时刻”生的——它不讲高大上的架构演进,只做一件事:用最直白的 Controller/View/Model 三层,配一套开箱即用的 Layui 表单和表格,把 Create、Read、Update、Delete 四个动作焊死在浏览器里。它适合刚从 Web Forms 转过来、对 MVC 生命周期还不熟的开发者,也适合需要快速交付 MVP 的小团队。项目没用 Entity Framework Code First 自动生成迁移脚本那种“玄学流程”,而是手写 SQL Server 数据访问层(DAL),连连接字符串都明晃晃写在web.config里;Layui 版本锁定在 2.8.18(避开了 3.x 的 breaking change),所有 JS/CSS 都本地化,不走 CDN。换句话说,你下载解压,改个数据库名,F5 一按,列表就出来了——没有“请先安装 Node.js”“请配置 webpack alias”“请运行 migration init”这类后悔药式前置条件。
2. 项目结构与技术栈拆解:为什么选 MVC 而不是 Core?为什么 Layui 不换 Vue?
2.1 目录结构:五层物理隔离,拒绝“上帝 Controller”
项目采用经典 ASP.NET MVC 5 的默认结构,但做了三处关键裁剪:
- 删除了
App_Start/BundleConfig.cs(Layui 所有资源本地化,不走 Bundle) - 合并了
Models下的ViewModel和Entity(新手阶段不引入 AutoMapper 层级,避免黑匣子) Controllers下仅保留ProductController.cs(示例模块),无 AccountController 或 Home 等干扰项
完整目录树如下(含关键文件说明):
/Controllers └── ProductController.cs ← 核心业务控制器,含 Index/Create/Edit/Delete 五个 Action /Models └── Product.cs ← EF 实体类,字段与数据库表完全映射(Id, Name, Price, Status) └── ProductContext.cs ← 继承 DbContext,只注册 Product DbSet,无其他模型 /DataAccess └── ProductDAL.cs ← 手写 ADO.NET 封装层,含 GetList()/GetById()/Insert()/Update()/Delete() 五个方法 /Views └── Product/ ├── Index.cshtml ← Layui 表格 + 搜索栏 + 新增按钮(含 layui.use(['table','form']...)) ├── Create.cshtml ← Layui 表单(form.on('submit', ...) + input[name="Name"] 等原生命名) ├── Edit.cshtml ← 同 Create,但 form action 指向 /Product/Edit/{id} └── _Layout.cshtml ← 极简母版页,只加载 layui.css + layui.js + 全局 JS 初始化 /Content └── layui/ ← 完整 Layui 2.8.18 发行包(含 css/layui.css, js/layui.js, modules/table.js 等) /Scripts └── product-index.js ← 专用于 Index 页面的 JS,初始化表格、绑定搜索、处理操作列事件提示:
DataAccess文件夹是本项目与“纯 MVC 教程”的分水岭。它不用 EF 的 SaveChanges(),而是用SqlCommand+SqlParameter手拼 SQL,比如UPDATE Product SET Name=@Name,Price=@Price WHERE Id=@Id。好处是执行计划透明、调试时能直接看到发出去的 SQL,坏处是你得自己写参数防注入——但本项目所有 DAL 方法都已强制使用SqlParameter,无需你补漏。
2.2 技术选型逻辑:MVC 5 是稳,Layui 是快
| 维度 | 选择依据 | 替代方案为何被弃用 |
|---|---|---|
| 框架 | ASP.NET MVC 5(.NET Framework 4.7.2) | ASP.NET Core MVC:需额外装 SDK、配置 Kestrel、处理跨域,对“今天部署明天上线”场景冗余;Web Forms:ViewState 体积大,Layui 表单绑定困难 |
| UI 框架 | Layui 2.8.18(本地化全量包) | Bootstrap 5:需额外引入 Popper.js,表格组件弱于 Layui 的table.render();Vue 3:需 setup script + ref 响应式,学习成本陡增 |
| 数据访问 | ADO.NET + 手写 DAL(非 EF Code First,非 Dapper) | Entity Framework:自动生成的 SQL 常含 N+1 查询,新手调优困难;Dapper:需手动写 SQL 字符串,易出拼接漏洞 |
| 部署方式 | IIS 传统站点部署(web.config 中<compilation targetFramework="4.7.2" />) | Azure App Service:需配置 .NET Framework 版本,且免费层不支持远程调试;Docker:.NET Framework 容器镜像体积大,启动慢 |
关键决策点在于:当你的目标是“让业务方能自己点按钮新增一条记录”,而不是“展示你精通微服务治理”时,少一层抽象就是少一个翻车点。比如ProductController.Index()方法里,它不调用IProductService,而是直接 newProductDAL(),再调dal.GetList()——代码丑,但断点打进去,哪一行卡住一目了然。
2.3 核心依赖清单:NuGet 包仅 3 个,无隐藏依赖
项目packages.config明确列出以下依赖(全部来自官方 NuGet 源,无第三方私服):
<package id="jQuery" version="3.6.0" targetFramework="net472" /> <package id="Microsoft.AspNet.Mvc" version="5.2.9" targetFramework="net472" /> <package id="Microsoft.AspNet.WebPages" version="3.2.9" targetFramework="net472" />注意:Microsoft.AspNet.Razor未单独列出,因它已包含在Microsoft.AspNet.Mvc5.2.9 中。
零容忍项:
- ❌ 无
Newtonsoft.Json(Layui 返回 JSON 用JsonResult,MVC 内置JavaScriptSerializer足够) - ❌ 无
Unity或Autofac(无 IOC 容器,Controller 构造函数无参数) - ❌ 无
log4net(错误日志直接写System.Diagnostics.Debug.WriteLine(),生产环境替换为EventLog即可)
提示:如果你在 VS2022 中打开此项目提示“需要安装 .NET Framework 4.7.2 Developer Pack”,请勿慌——这是正常现象。去微软官网下载对应 installer 即可,安装过程 3 分钟,比配通一个 Webpack loader 快 10 倍。
3. 运行前必改的 3 处配置:数据库、端口、Layui 路径
3.1 数据库连接字符串:改web.config里的 connectionStrings 节点
打开/web.config,定位到<connectionStrings>节点,将默认值:
<add name="DefaultConnection" connectionString="Data Source=.;Initial Catalog=DemoDB;Integrated Security=true;" providerName="System.Data.SqlClient" />替换成你本地 SQL Server 实例的实际配置。常见修改场景:
- SQL Server Express:
Data Source=YOUR-PC\SQLEXPRESS;... - 用户名密码登录:
Data Source=192.168.1.100;Initial Catalog=DemoDB;User ID=sa;Password=yourpass; - 端口非 1433:
Data Source=192.168.1.100,1434;...(注意逗号,非冒号)
注意:
Initial Catalog(即数据库名)必须存在。若不存在,请先用 SSMS 创建空库DemoDB,再运行下方建表 SQL。
3.2 数据库建表脚本:复制粘贴即可,无外键无索引
在 SQL Server Management Studio 中执行以下脚本(共 1 张表,5 个字段):
USE [DemoDB] GO CREATE TABLE [dbo].[Product]( [Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY, [Name] [nvarchar](100) NOT NULL, [Price] [decimal](18, 2) NOT NULL DEFAULT ((0)), [Status] [tinyint] NOT NULL DEFAULT ((1)), -- 1:上架, 0:下架 [CreateTime] [datetime] NOT NULL DEFAULT (getdate()) ) GO -- 插入测试数据(可选) INSERT INTO [Product] ([Name],[Price],[Status]) VALUES ('iPhone 15', 5999.00, 1), ('MacBook Pro', 12999.00, 1)关键设计点:
Status字段用tinyint而非bit,因为 Layui 表单中下拉框常需多状态(如 0=禁用,1=启用,2=审核中),bit只能存 0/1;CreateTime默认getdate(),Controller 层不传该字段,避免时区问题;- 无外键约束,因本模板暂不涉及关联查询(如 Category 表),后续扩展时再加。
3.3 Layui 资源路径校验:确认layui.js加载无 404
打开/Views/Shared/_Layout.cshtml,检查以下两行是否指向正确路径:
<link rel="stylesheet" href="~/Content/layui/css/layui.css"> <script src="~/Content/layui/layui.js"></script>若你解压后把layui文件夹放到了其他位置(如/Scripts/layui/),则必须同步修改这两行。验证方法:F12 打开浏览器开发者工具 → Network 标签页 → 刷新页面 → 查看layui.css和layui.js是否返回 200。若返回 404,请立即修正路径——这是新手跑不通的最高频原因。
3.4 启动项目:IIS Express vs 本地 IIS,选哪个?
- 开发阶段(推荐):VS 内置 IIS Express。右键项目 → “设为启动项目” → 按 Ctrl+F5(不调试)。URL 通常为
http://localhost:50000/。 - 演示阶段(推荐):发布到本地 IIS。右键项目 → “发布” → 选择“文件系统” → 目标位置设为
C:\inetpub\wwwroot\productadmin→ 在 IIS 管理器中添加网站,物理路径指向该文件夹,绑定http:*:8080:。
提示:若用 IIS Express 启动时报错 “无法启动 IIS Express”,请检查端口是否被占用(如 Skype 占用 80 端口)。解决方案:右键项目 → “属性” → “Web” 选项卡 → 修改“项目 URL”端口号为
50001或其他空闲端口。
4. 核心代码逐行解析:从 Controller 到 Layui 表格渲染的完整链路
4.1 ProductController:五个 Action 如何串联起 CRUD
/Controllers/ProductController.cs是整个流程的中枢。我们以Index和Edit为例,逐行拆解:
public class ProductController : Controller { // GET: Product public ActionResult Index() { var dal = new ProductDAL(); // 1. 实例化数据访问层 var list = dal.GetList(); // 2. 调用 DAL 获取全部产品(返回 List<Product>) return View(list); // 3. 将数据传给 Index.cshtml 视图 } // GET: Product/Edit/5 public ActionResult Edit(int id) { var dal = new ProductDAL(); var model = dal.GetById(id); // 4. 根据 ID 查询单条记录 if (model == null) // 5. ID 不存在则跳转 404 { return HttpNotFound(); } return View(model); // 6. 将单条数据传给 Edit.cshtml } // POST: Product/Edit/5 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit([Bind(Include = "Id,Name,Price,Status")] Product product) { if (ModelState.IsValid) // 7. MVC 自动验证 [Required] 等特性 { var dal = new ProductDAL(); dal.Update(product); // 8. 调用 DAL 更新数据库 return RedirectToAction("Index"); // 9. 重定向回列表页,防止重复提交 } return View(product); // 10. 验证失败则重新显示表单(含错误提示) } }关键参数说明:
[Bind(Include = "Id,Name,Price,Status")]:显式指定允许绑定的字段,防止“ overposting 攻击”(如恶意提交Status=999关闭所有商品);[ValidateAntiForgeryToken]:配合@Html.AntiForgeryToken()使用,防御 CSRF;return RedirectToAction("Index"):这是 MVC 的黄金法则——POST 成功后必须重定向,否则刷新页面会重复提交。
4.2 ProductDAL:手写 ADO.NET 的安全写法
/DataAccess/ProductDAL.cs中Update方法是重点,它展示了如何安全地执行参数化更新:
public void Update(Product model) { string sql = "UPDATE Product SET Name=@Name,Price=@Price,Status=@Status WHERE Id=@Id"; using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString)) { using (var cmd = new SqlCommand(sql, conn)) { // 11. 为每个参数显式添加 SqlParameter,类型与长度严格匹配 cmd.Parameters.Add(new SqlParameter("@Name", SqlDbType.NVarChar, 100) { Value = model.Name }); cmd.Parameters.Add(new SqlParameter("@Price", SqlDbType.Decimal) { Value = model.Price }); cmd.Parameters.Add(new SqlParameter("@Status", SqlDbType.TinyInt) { Value = model.Status }); cmd.Parameters.Add(new SqlParameter("@Id", SqlDbType.Int) { Value = model.Id }); conn.Open(); cmd.ExecuteNonQuery(); // 12. 执行非查询命令 } } }为什么不用cmd.Parameters.AddWithValue()?
AddWithValue会自动推断参数类型,可能导致 SQL Server 执行计划缓存污染(如第一次传"abc"推断为varchar(3),第二次传"hello world"就触发重编译);- 本写法显式声明
SqlDbType.NVarChar, 100,与数据库字段nvarchar(100)完全一致,性能更稳。
4.3 Index.cshtml:Layui 表格如何接收 MVC 的 JSON 数据
/Views/Product/Index.cshtml的核心是layui.table.render(),它要求后端返回特定 JSON 结构:
<table id="productTable" lay-filter="productFilter"></table> <script> layui.use(['table', 'form'], function () { var table = layui.table; table.render({ elem: '#productTable', url: '/Product/GetList', // 13. 请求地址,返回 JSON 格式数据 page: true, cols: [[ { field: 'Id', title: 'ID', width: 80, sort: true }, { field: 'Name', title: '名称', edit: 'text' }, // 14. 双击可编辑 { field: 'Price', title: '价格', templet: '<div>¥{{d.Price}}</div>' }, { field: 'Status', title: '状态', templet: '#statusTpl' }, // 15. 使用模板渲染 { title: '操作', toolbar: '#barDemo', width: 150 } ]] }); }); </script> <!-- 状态模板 --> <script type="text/html" id="statusTpl"> {{# if(d.Status == 1){ }} <span class="layui-badge layui-bg-green">上架</span> {{# } else { }} <span class="layui-badge layui-bg-gray">下架</span> {{# } }} </script> <!-- 工具栏模板 --> <script type="text/html" id="barDemo"> <a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a> <a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a> </script>后端GetListAction 如何响应?
在ProductController中新增:
public JsonResult GetList(int page = 1, int limit = 10) { var dal = new ProductDAL(); var list = dal.GetList(); // 注意:此处未分页!真实项目需加 offset/limit // Layui 分页要求格式:{code:0, msg:"", count:总条数, data:[{},{},...]} return Json(new { code = 0, msg = "", count = list.Count, data = list }, JsonRequestBehavior.AllowGet); }注意:
JsonRequestBehavior.AllowGet是关键,否则 AJAX 请求会报 405 错误。Layui 的url参数本质是 GET 请求,必须允许。
5. 避坑指南:那些让你调试到凌晨三点的典型问题
5.1 现象:点击“编辑”按钮无反应,F12 看 Network 无请求发出
原因:Index.cshtml中layui.use(['table','form'])未正确加载form模块,导致lay-event="edit"绑定失败。
解决:确认<script>标签内layui.use的数组包含'form',且table.render()后必须调用table.on('tool(productFilter)', function(obj){...})绑定事件。完整事件绑定代码如下:
table.on('tool(productFilter)', function (obj) { var data = obj.data; // 获得当前行数据 var layEvent = obj.event; // 获得 lay-event 对应的值 if (layEvent === 'edit') { location.href = '/Product/Edit/' + data.Id; // 跳转到编辑页 } else if (layEvent === 'del') { layer.confirm('确定删除?', function (index) { $.get('/Product/Delete/' + data.Id, function (res) { if (res.code === 0) { obj.del(); // 删除对应行 layer.close(index); } }); }); } });5.2 现象:新增商品时提示“Validation failed”,但所有字段都填了
原因:Product.cs模型类中Price字段未加[Range]特性,而数据库是decimal(18,2),MVC 默认认为0是无效值。
解决:在Product.cs中为Price添加验证:
public class Product { public int Id { get; set; } [Required(ErrorMessage = "名称不能为空")] [StringLength(100, ErrorMessage = "名称不能超过100字符")] public string Name { get; set; } [Required(ErrorMessage = "价格不能为空")] [Range(0.01, 99999999.99, ErrorMessage = "价格必须在0.01~99999999.99之间")] public decimal Price { get; set; } [Required(ErrorMessage = "状态不能为空")] public byte Status { get; set; } // 注意:tinyint 对应 C# 的 byte,非 int }5.3 现象:Layui 表格显示“数据接口异常”,Console 报错Uncaught TypeError: Cannot read property 'length' of undefined
原因:GetListAction 返回的 JSON 中data字段为null,而非空数组[]。Layui 表格要求data必须是数组。
解决:修改GetList方法,确保list为空时返回空数组:
public JsonResult GetList(int page = 1, int limit = 10) { var dal = new ProductDAL(); var list = dal.GetList(); // 强制转换为 List<Product>,避免 null var dataList = list ?? new List<Product>(); return Json(new { code = 0, msg = "", count = dataList.Count, data = dataList }, JsonRequestBehavior.AllowGet); }5.4 现象:修改商品后,列表页价格显示仍为旧值,刷新页面才更新
原因:Layui 表格启用了cache: true(默认开启),会缓存 AJAX 响应,导致GetList返回旧数据。
解决:在table.render()配置中显式关闭缓存:
table.render({ elem: '#productTable', url: '/Product/GetList', cache: false, // 关键!禁用缓存 page: true, // ... 其他配置 });5.5 现象:部署到 IIS 后,所有 Layui 图标(如 ✖️)显示为方块
原因:IIS 默认未启用woff2字体 MIME 类型,Layui 的图标字体iconfont.woff2被拒载。
解决:在 IIS 管理器中,选中网站 → “MIME 类型” → 右侧“添加” → 扩展名填.woff2,MIME 类型填font/woff2→ 点击确定。重启网站即可。
6. 二次开发实战:如何在 30 分钟内接入新模块(以“订单管理”为例)
6.1 新增 Order 模块的四步法
假设你要增加“订单管理”功能,复用本项目的结构,只需四步:
步骤 1:创建数据库表(SQL Server)
USE [DemoDB] GO CREATE TABLE [dbo].[Order]( [Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY, [OrderNo] [varchar](20) NOT NULL, [ProductId] [int] NOT NULL, [Quantity] [int] NOT NULL DEFAULT ((1)), [TotalAmount] [decimal](18, 2) NOT NULL, [CreateTime] [datetime] NOT NULL DEFAULT (getdate()) ) GO步骤 2:生成 Model 与 DAL(手写,非 EF)
/Models/Order.cs:定义Id,OrderNo,ProductId,Quantity,TotalAmount,CreateTime属性;/DataAccess/OrderDAL.cs:复制ProductDAL.cs,将所有Product替换为Order,SQL 语句改为SELECT * FROM [Order]等;/Controllers/OrderController.cs:复制ProductController.cs,Action 名称与路由保持一致(Index,Create,Edit,Delete)。
步骤 3:复用 Layui 模板(最小改动)
/Views/Order/Index.cshtml:复制/Views/Product/Index.cshtml,仅修改table.render()的url为'/Order/GetList',cols字段改为OrderNo,ProductId等;/Views/Order/Create.cshtml:复制/Views/Product/Create.cshtml,将input[name="Name"]改为input[name="OrderNo"],并添加ProductId下拉框(需在 Controller 中ViewBag.ProductList传入)。
步骤 4:添加导航菜单(修改_Layout.cshtml)
在<nav class="layui-nav">内追加:
<li class="layui-nav-item"> <a href="/Order/Index">订单管理</a> </li>提示:不要试图用
@Html.ActionLink,本项目所有导航都是纯 HTML<a>标签,杜绝 Razor 语法复杂度。
6.2 关联查询实战:在订单列表显示商品名称
Order表有ProductId,但列表需显示Product.Name。手写 DAL 中加入 JOIN 查询:
// /DataAccess/OrderDAL.cs public List<OrderWithProductName> GetOrderListWithProduct() { string sql = @" SELECT o.Id, o.OrderNo, p.Name AS ProductName, o.Quantity, o.TotalAmount, o.CreateTime FROM [Order] o INNER JOIN Product p ON o.ProductId = p.Id"; // ... 执行查询,返回 List<OrderWithProductName> }对应新建OrderWithProductName.cs模型(非数据库实体):
public class OrderWithProductName { public int Id { get; set; } public string OrderNo { get; set; } public string ProductName { get; set; } public int Quantity { get; set; } public decimal TotalAmount { get; set; } public DateTime CreateTime { get; set; } }OrderController.Index()中调用dal.GetOrderListWithProduct(),视图中cols字段直接用ProductName。
6.3 权限控制轻量接入:基于 Session 的角色拦截
若需区分“管理员”和“普通用户”,在Global.asax.cs的Application_AuthenticateRequest中添加:
protected void Application_AuthenticateRequest(Object sender, EventArgs e) { if (Context.User != null && Context.User.Identity.IsAuthenticated) { // 假设登录后存 Session["Role"] = "Admin" 或 "User" var role = Context.Session["Role"] as string; if (!string.IsNullOrEmpty(role)) { var identity = new GenericIdentity(Context.User.Identity.Name); var principal = new GenericPrincipal(identity, new[] { role }); Context.User = principal; } } }然后在OrderController上加特性:
[Authorize(Roles = "Admin")] // 仅管理员可访问 public class OrderController : Controller { ... }从那以后我每次接到“加个新模块”的需求,都强制走一遍这四步法:建表 → 写 DAL → 拷 Controller/View → 改导航。它不优雅,但能保证我在咖啡凉透前把链接发给测试同学。希望帮到你。
本文还有配套的精品资源,点击获取