简介这是一套基于ASP.NET与EasyUI的企业级管理系统完整源码主要面向.NET开发人员、毕业设计学生以及有二次开发需求的企业团队。项目采用MVC分层架构Controller负责请求调度Model与DAL层处理业务规则和数据库访问View层则借助EasyUI的表格、下拉框、对话框等组件搭建后台管理界面并集成ECharts统计图可对业务数据进行可视化分析。资源共1686个文件压缩包约27.9MB其中包含624个css样式文件、502个png图片、162个dll程序集、64个js脚本、50个cs源文件、20个cshtml视图页面、1个sql数据库脚本dll程序集覆盖数据访问与公共组件css/png/js构成前端UI资源sql脚本便于快速初始化数据库。目录按表现层、业务层、数据访问层等模块划分适合对照学习ASP.NET MVC的完整开发流程也可直接修改扩展用于实际管理系统。目前已有271人学习下载对希望理解前后端整合和图表展示逻辑的读者有较高参考价值。1. 一套还在跑的传统后台管理系统ASP.NET MVC EasyUI 的骨架接到这套基于 ASP.NET 与 EasyUI 的管理系统源码时第一感觉是“旧”但把项目文件列表翻开之后发现它其实保留了企业后台管理系统最典型的一套骨架MVC 分层、三层架构DAL/BLL/UI、数据库脚本、ECharts 统计图。这样的项目到现在仍然是很多企业 OA、进销存、选课管理系统的基础模板尤其适合需要拿来做课程设计、毕业设计或者内部系统二次开发的场景。它的价值不在于“新技术”而在于结构完整、可以直接跑通。Global.asax、eUI.BLL、eUI.DAL、easyUITest.csproj 这些文件说明它同时存在 WebForms 与 MVC 痕迹也可能是两个工程放在同一个解决方案里。下面几章会按阅读源码的顺序把业务分层、分页契约、动态菜单、统计图接口逐一拆开最后给出这套源码里最容易被忽略但实际会坑到人的几个细节。2. 读三层源码结构前先搞清楚 easyUITest 和 eUI.UI 的关系解压 rar 之后解决方案里通常会看到两个 Web 项目easyUITest和eUI.UI。前者名字上像是 EasyUI 的测试工程后者才是真正可部署的主站点。遇到这种情况不要直接启动解决方案里第一个项目优先把eUI.UI设为启动项目否则打开的是测试页而不是登录页。2.1 从 csproj 文件识别主项目用文本编辑器打开eUI.UI.csproj里面会有ProjectGuid、OutputPath和一堆Compile Include节点。如果看到Global.asax和Views/Web.config同时出现在编译项里说明这是标准的 ASP.NET MVC 工程。相比之下easyUITest.csproj如果只有少量页面和bin引用那它的作用就是给 EasyUI 组件做本地演示不影响业务功能。2.2 三层中 DAL、BLL、Common 各自的职责eUI.DAL负责数据访问eUI.BLL负责业务规则eUI.Common放公共方法比如加密、分页参数转换、JSON 序列化辅助。在实际代码里DAL 最常见的写法是“手写 ADO.NET SQL 字符串拼接”而不是 EF。好处是便于理解 SQL 执行过程坏处是参数化稍不注意就会留下注入点。搜索SqlCommand或SqlParameter能快速判断这个 DAL 层写得多严谨。public DataTable GetList(string where) { string sql SELECT * FROM T_User WHERE 11 where; using (SqlConnection conn new SqlConnection(connStr)) { SqlCommand cmd new SqlCommand(sql, conn); SqlDataAdapter da new SqlDataAdapter(cmd); DataTable dt new DataTable(); da.Fill(dt); return dt; } }这段代码是“能跑但需要改”的典型where直接拼进 SQL如果调用处传入的是用户可控值就存在注入风险。正确做法是把条件部分改成keyword参数然后用cmd.Parameters.AddWithValue传值。2.3 bin 目录与 ResolveAssemblyReference.cache 残留项目列表里出现大量*.csprojResolveAssemblyReference.cache文件这是 Visual Studio 在编译时生成的程序集引用缓存不是源代码也不应该提交到版本库。清理方法是关闭 VS删除所有obj、bin目录再把以.csprojResolveAssemblyReference.cache结尾的文件全部删除。find . -name *.csprojResolveAssemblyReference.cache -type f -delete find . -name *.cache -type f -delete删除后重新打开解决方案会触发一次完整引用解析编译速度会变慢但能保证引用关系是干净的。确认哪个是主项目还有一招看Web.config文件的位置MVC 主站点一定在根目录包含Views和Controllers文件夹而测试工程通常只有一个Default.aspx或少量静态 HTML。3. 数据库与主界面把用户、菜单、权限表先摸清这套系统的数据库文件.mdf或.sql解压后可以直接附加到 SQL Server。在 SSMS 里附加之前要先用sp_attach_db或图形界面确认日志文件路径正确。附加成功之后优先查三张表用户表、角色表、菜单表。3.1 典型的权限模型数据库里通常会看到类似Users、Roles、Sys_Menu加一对关联表的组合。菜单表设计成“父子结构”最方便 EasyUI 的 Tree 组件直接渲染字段名类型说明MenuIdint主键ParentIdint父级菜单 ID0 为根MenuNamenvarchar(50)菜单名称MenuUrlnvarchar(200)页面地址或 ActionSortNoint排序号3.2 权限菜单的 SQL 写法查询当前用户可见菜单时最直接的做法是关联角色权限表SELECT DISTINCT m.MenuId, m.ParentId, m.MenuName, m.MenuUrl, m.SortNo FROM dbo.Sys_Menu m LEFT JOIN dbo.Role_Menu rm ON m.MenuId rm.MenuId LEFT JOIN dbo.User_Role ur ON rm.RoleId ur.RoleId WHERE ur.UserId userId AND m.IsShow 1 ORDER BY m.SortNo;IsShow字段用来控制菜单是否显示比直接删除记录更灵活。这里没有把 SQL 拼进字符串而是以userId参数传入调用前的逻辑是先从Session[UserId]取出登录用户 ID再传给 DAL 层执行。注意查询结果要构造成 EasyUI Tree 需要的嵌套结构通常是在 C# 里递归转一次 JSON。3.3 登录态与 Global.asax 的事件处理Global.asax里一般会处理Application_AuthenticateRequest或Application_Start。在这个项目里登录验证依赖Session或者FormsAuthentication手动在控制器里判断Session[UserId] null就跳转到登录页。缺点是每个 Action 都要写一遍更好的做法是用MVC 过滤器public class LoginFilter : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { if (HttpContext.Current.Session[UserId] null) { filterContext.Result new RedirectResult(/Account/Login); return; } base.OnActionExecuting(filterContext); } }控制器里只需要打上[LoginFilter]特性就能整类拦截。这套源码如果用的是 MVC 5过滤器写法基本一致不用改太多代码。3.4 主界面 Layout 与 EasyUI 组件主布局页面通常放在Views/Shared/_Layout.cshtml里用 EasyUI 的layout插件把页面分成 north、west、center 三个区域。左侧菜单用datagrid-tree或tree组件从/Menu/GetMenuTree接口拉取。这里的重点在于后端返回的数据格式必须匹配 EasyUI 要求id、text、children这三个字段不能少。$(#treeMenu).tree({ url: /Menu/GetMenuTree, animate: true, onSelect: function (node) { if (node.url node.url ! ) { $(#mainTabs).tabs(add, { title: node.text, href: node.url, closable: true }); } } });onSelect事件触发时如果节点没有url说明是父级菜单只展开子项如果有url则新建一个 Tab。这种方式比 iframe 嵌套更符合 EasyUI 多页签习惯也方便在统计图页面里嵌多个报表页面时保持左侧菜单不动。4. 统计图功能从 SQL 聚合到 ECharts 渲染的完整链路项目里的统计图用的是 ECharts和 EasyUI 配套使用非常常见。难点不在前端配置而在后端聚合查询怎么写、返回 JSON 用什么格式、前端如何解析后再渲染。4.1 后端统计接口统计图接口通常是单独一个StatisticsController返回JsonResult。比如统计近 12 个月每个月的订单数public JsonResult OrderMonthly() { string sql SELECT CONVERT(VARCHAR(7), OrderDate, 120) AS MonthKey, COUNT(1) AS OrderCount FROM dbo.Orders WHERE OrderDate DATEADD(MONTH, -11, GETDATE()) GROUP BY CONVERT(VARCHAR(7), OrderDate, 120) ORDER BY MonthKey;; DataTable dt new DalBase().Query(sql); var list new Listobject(); foreach (DataRow row in dt.Rows) { list.Add(new { month row[MonthKey].ToString(), count Convert.ToInt32(row[OrderCount]) }); } return Json(list, JsonRequestBehavior.AllowGet); }CONVERT(VARCHAR(7), OrderDate, 120)是把日期截成yyyy-MM这样分组聚合自然得到月度数据。注意前端 AJAX 调用这个接口时GET请求必须写JsonRequestBehavior.AllowGet否则会被 ASP.NET MVC 默认拒绝。4.2 ECharts 初始化与数据加载页面引入 ECharts 文件后先按固定宽高初始化容器再请求后端接口var chart echarts.init(document.getElementById(statChart)); $.getJSON(/Statistics/OrderMonthly, function (data) { var months []; var counts []; $.each(data, function (i, item) { months.push(item.month); counts.push(item.count); }); chart.setOption({ title: { text: 近 12 月订单量 }, tooltip: { trigger: axis }, xAxis: { type: category, data: months }, yAxis: { type: value }, series: [{ type: bar, data: counts }] }); });这里要特别留意统计图所在 Tab 若一开始处于隐藏状态EasyUI Tabs 切换回来之后图表宽度可能计算成 0导致渲染空白。常见做法是在 Tab 的onSelect事件里调用chart.resize()。4.3 饼图与多图布局如果统计图页面要放多个图表通常用 EasyUItabs或简单div display:block分块展示。饼图接口返回格式一般是[{ name: 类型, value: 40 }]后端只要把聚合后的CategoryName和计数值按此格式输出即可。series: [{ type: pie, radius: [30%, 70%], data: data }]内外半径形成环形比默认实心饼视觉上更合适后台数据看板。这段代码需要和接口返回的字段名严格对齐如果后端改名了前端data里没有name和value饼图会直接空白。4.4 接口性能与缓存处理统计页在数据量大时SQL 聚合很耗时间。一般不建议直接改 SQL 强行优化先确认查询条件是否走索引。这里较快的改进方式是在后端加OutputCache特性[OutputCache(Duration 300)] public JsonResult OrderMonthly()这个特性会让同一用户在 5 分钟内重复请求直接走缓存适合数据实时性要求不高的看板页。注意调试期间最好设成 0否则改前端代码后接口结果不更新很容易误判成代码问题。5. 把 DataGrid 换成 TreeGrid 前的三个检查点最后一章讲一个实战中验证这套源码最常用的方式把普通列表升级为树形表格顺便验证整个 MVC EasyUI 链路是否通畅。TreeGrid 能展示父子层级数据适合订单明细、库存分类这类有嵌套结构的业务表。5.1 检查点一返回格式是否是 TreeGrid 需要的结构EasyUI TreeGrid 要求 JSON 里包含children数组并且每一行要有唯一的id。普通 DataGrid 返回的是total rows如果直接把后台接口换给 TreeGrid 使用前端拿不到children树形结构就出不来。$(#grid).treegrid({ url: /Product/GetTreeGrid, idField: id, treeField: name, columns: [[ { field: name, title: 分类名称, width: 200 }, { field: count, title: 数量, width: 100 } ]] });5.2 检查点二递归树的层级控制后端构造children时要注意停递归的条件。如果数据库里有一条记录的ParentId指向自身SQL 查询会陷入死循环。最简单的防线是加一个深度字段private void BuildTree(DataTable dt, DataRow parentRow, Listobject result, int depth) { if (depth 6) return; foreach (DataRow row in dt.Select(ParentId parentRow[Id].ToString())) { var node new Dictionarystring, object(); node[id] row[Id]; node[name] row[Name]; node[children] new Listobject(); BuildTree(dt, row, (Listobject)node[children], depth 1); result.Add(node); } }这里的depth 6是经验值普通后台管理系统的菜单或分类层级一般不会超过 6 层。如果业务 Category 只有两级设到 3 就够了写太大会让异常数据多跑几层查询。5.3 检查点三id是否保持唯一TreeGrid 的idField必须是全表唯一很多新手会把MenuId和ParentId搞混导致选中子节点时父节点也被展开。验证方式是直接在浏览器控制台里执行一段脚本var rows $(#grid).treegrid(getData); var ids rows.map(function (r) { return r.id; }); var unique new Set(ids); console.log(重复数: (ids.length - unique.size));如果输出大于 0说明 SQL 或递归拼接逻辑里存在重复 ID 的记录需要优先排查UNION查询或者多级分类表数据。把这三个检查点逐一对齐TreeGrid 基本不需要再改动前端框架代码整个 MVC EasyUI SQL Server 的链路就能稳定跑起来。本文还有配套的精品资源点击获取