简介:基于 ASP.NET 8.0 的开源后台管理框架,整合 MVC、API、SqlSugar 与 LayUI,面向需要快速交付企业级 Web 应用的 C#/.NET 开发团队,目标是减少权限、表单、数据隔离等基础功能的重复搭建。框架内置字段级数据权限、流程表单设计、多数据库多租户支持,并兼容 .Net4.5、.NetCore3.1、.Net5、.Net6、.Net8 等多个版本,ORM 还集成 Chloe,便于按项目需求灵活选型。压缩包共 668 个文件,约 143.61MB,以 285 个 C# 源码文件、108 个 Razor 视图模板、92 个 JavaScript 脚本和 40 个 CSS 样式为主,同时包含配置文件、数据库脚本、Dockerfile 及项目说明文档,结构清晰,适合直接导入开发环境运行体验。压缩包内还包含种子数据、权限按钮配置、单表模板、RabbitMQ 辅助类、流程实例服务等示例代码,便于理解整体架构。已有 483 人学习,代码完全开源,可自由下载、修改和分发,适合需要快速搭建后台权限体系、审批流程或多租户 SaaS 平台的团队,能显著节省前期设计与开发时间。 做C#.NET开发的朋友应该都有类似的经历——接手的每一个新项目,数据库操作、登录认证、后台管理页面、权限分配、增删改查……这些代码翻来覆去地写,换个项目就得重来一遍。尤其是给中小企业做管理系统,需求翻来覆去就是“增删改查+报表+权限”,但每一套写下来都能耗掉小半个月。今天要说的这套基于ASP.NET 8.0 MVC + API + SqlSugar + LayUI的框架,就是冲着这个痛点去的,源代码完全开源,拿来就能改,改了就能跑。
框架的定位很明确:给你一个带有完整基础设施的起步模板。它不是一个重量级低代码平台,而是把项目里最耗时的那些"地基活"全干完了——ORM封装、JWT认证、用户角色权限、后台管理界面、统一响应格式、代码生成器,全部开箱即用。适合三类人使用:被重复的CRUD折磨的中小团队开发、准备从.NET Framework迁到.NET 8的老项目维护者、以及想系统学习MVC+API+SqlSugar+LayUI这套组合的技术爱好者。
1. 这套框架解决的是什么问题
1.1 重复劳动到底重复在哪
你回忆一下,之前做过多少个类似的系统?进销存、OA、资产管理、客户管理……业务逻辑千变万化,但骨架永远是那几个模块:登录注销、用户管理、角色分配、菜单权限、操作日志、数据字典。如果这些代码每次都手写,那不光浪费时间,还会因为每个项目的写法不一致,给后续维护埋雷。
这个框架用几层手段把这些重复工作压到最低:数据库层面用SqlSugar做CodeFirst初始化,表结构变了自动同步;接口层面用泛型仓储+统一基类控制器,新增一个业务表只需要写几行继承代码;前端层面用LayUI封装好了表格增删改查组件,后端把数据格式返回对了,表格自动渲染;最后再用代码生成器把Controller、Service、ViewModel一次生成出来,再复杂的单表业务也能做到"十分钟出一个模块"。
1.2 为什么选这四件套而不是别家方案
先说说框架选型,这是很多人问的第一个问题。后端技术栈里有EF Core这个微软官方ORM,为什么选SqlSugar?前端明明有Vue3+ElementPlus这一大堆生态,为什么选LayUI这种老牌的jQuery系框架?
我的看法是这样的:选型要看团队和业务场景。SqlSugar是国内团队维护的ORM,文档是中文的,语法更贴合国内开发者的习惯,特别是在多库支持、分库分表、读写分离这块比EF Core配置起来简单得多。而且SqlSugar的实体特性支持很直接,从数据库反向生成实体类的工具链也很完善,适合快速交付的项目。
前端选LayUI的理由更实际——这类框架的目标场景是后台管理系统,用户量不大,交互不需要太花哨。LayUI最大的优势是资源占用小、学习成本低、静态资源直接引CDN就能用。团队里如果大家都会写HTML+jQuery,那LayUI上手的陡峭程度几乎为零。相比之下,Vue项目要配Node环境、要处理打包构建,对于"主要精力应该放在业务逻辑上"的管理系统来说,反而多了一层负担。
2. 整体架构设计与分层思路
2.1 项目分层结构
框架在代码组织上采用了经典的分层结构,它把项目拆成了几个独立工程。我大概拆一下它的目录和职责:
Xxx.Core:领域层,放实体类、枚举、通用基类。这一层不引用任何上层项目,是纯粹的模型定义。Xxx.Application:应用层,放业务逻辑接口和实现,比如用户服务、角色服务、日志服务。控制器只调这层的接口,不直接操作数据库。Xxx.Web:表现层,包含MVC控制器、Web API控制器、LayUI页面、静态资源,也是整个框架的入口。Xxx.Common:公共层,放JWT工具、Redis缓存封装、Excel导入导出、全局异常处理过滤器等横切关注点。
你可能注意到,这里并没有单独拆出仓储层。这是SqlSugar的一个特点:它本身就是一个仓储,ISqlSugarClient直接提供CURD方法,完全不需要再包一层Repository接口来掩盖它。过度设计是所有框架最容易踩的坑,SqlSugar已经提供了足够的抽象程度,我们再包一层反而让代码变得绕。
2.2 一条请求走过的完整链路
以"管理员给用户分配角色"这个功能为例,看一条请求在框架里是怎么走的。用户在LayUI的页面勾选角色,点保存,前端脚本收集用户ID和角色ID数组,用AJAX发到API控制器。请求先经过全局中间件做JWT令牌校验,令牌合法后进入控制器的SaveUserRoles方法。控制器不写业务,只把参数交给应用层的UserService,应用层用事务把该用户原有的角色关联记录删掉,再批量插入新的关联记录。最后返回一个统一响应对象ResultModel,前端拿到后刷新表格并弹出成功提示。
整个过程涉及前端渲染、接口鉴权、业务事务、多表操作,每一环都是可以复用的通用逻辑。框架把每一环都封装成了约定,你新写的业务代码只要遵循这个链路,就能自动获得日志记录、异常处理、统一响应这些能力。
3. 核心功能模块拆解
3.1 基于SqlSugar的数据访问封装
SqlSugar在框架里承担的不只是简单的增删改查,我特别看重它的几个能力。第一个是CodeFirst建表,直接用实体类同步表结构,开发阶段改个字段加个属性,运行起来自动执行迁移,省掉了一遍遍手写SQL的烦恼。第二个是复杂查询支持,比如分页查询可以用ToPageListAsync一步到位,条件拼接用WhereIF特别顺手。
框架里把数据库连接归到了SqlSugarScope这个单例对象里管理,用它来保证多线程下获取的连接实例是正确的。这里有个值得注意的细节:SqlSugar的实例模式有单例和Scoped两种,单例模式下内部做了上下文隔离,所以可以放心地注入成静态服务。下面是一个典型的仓储基类写法:
public class BaseRepository<T> : SimpleClient<T> where T : class, new() { protected readonly SqlSugarScope _db; public BaseRepository(ISqlSugarClient db) { base.Context = db as SqlSugarScope; _db = db as SqlSugarScope; } public async Task<PageResult<T>> GetPageAsync(PageRequest req, Expression<Func<T, bool>> where) { RefAsync<int> total = 0; var list = await _db.Queryable<T>() .Where(where) .OrderBy($"create_time desc") .ToPageListAsync(req.PageIndex, req.PageSize, total); return new PageResult<T> { List = list, Total = total }; } }这段代码把分页、排序、条件查询收敛到了基类,业务仓储继承之后不需要再重复写分页逻辑。大家在复制这个思路时注意一点:OrderBy里面如果直接用字符串排序,一定要校验字段名白名单,否则用户传个orderBy=xxx;drop table进来,SqlSugar虽然做了参数化,但动态排序字段还是存在注入风险。
3.2 JWT认证与权限控制
管理系统不能没有登录态,框架在这方面采用的是JWT无状态认证。用户登录成功后,后端签发Token,前端把Token存在localStorage,每次AJAX请求都在Header里带上Authorization: Bearer xxx。后端在Program.cs里配上认证中间件,然后API控制器或Action上用[Authorize]特性控制访问。
权限控制思路也比较实用——不搞复杂的OAuth授权码流程,直接做"用户-角色-菜单-按钮"四层。用户表关联角色表,角色表关联菜单权限表,菜单表里有个permission_code字段,比如user:add、role:delete这种东西。框架自定义了一个[PermissionFilter]过滤器,在Action执行前检查当前用户是否拥有该权限码,没有就直接返回401或403。按钮级别的控制在LayUI前端用条件渲染来做,有权限码就渲染按钮,没有就不渲染,后端的权限校验同时兜底。
这里建议接手框架的朋友把Token过期时间、刷新策略这几个参数先搞清楚。框架默认Token有效期是2小时,但实际部署时要注意服务器时间和客户端时间的偏移,时间不同会导致JWT立刻过期或无限期有效,这是特别常见又特别隐性的问题。
3.3 LayUI后台模板与动态菜单
LayUI在框架里扮演的是后台整体UI的角色。布局上用的是典型的左右结构:左侧菜单树加顶部导航栏,内容区用iframe嵌入各个模块页面。菜单不是写死的,是根据当前用户角色动态从数据库加载的。具体实现是:前端登录后请求一次/api/menus/current接口,后端按当前用户查询可见菜单,按父子ID组装成树形结构返回,前端用LayUI的tree组件渲染。
表格部分统一用了table模块,每次进入列表页时自动请求后端分页接口,后端按layui规定好的参数名(page、limit)接收,返回{"code":0, "msg":"", "count":100, "data":[...]}格式,表格就能直接渲染。所以你会发现框架里的所有列表页几乎长得一模一样,这就是因为封装的太彻底了——你要做的只是配置表格的列字段。
form模块负责表单校验和弹窗提交。我常用的组合是layer.open开一个iframe弹窗做新增/编辑,弹窗里放一个表单,提交时用form.on('submit)'监听,把表单序列化成JSON数据AJAX到后端接口,成功后关闭弹窗并table.reload()刷新列表。这套交互Template已经封装成了标准写法,新开发模块直接拷贝改个URL就能用。
3.4 代码生成器:十分钟生成一个完整模块
这是这个框架最提效的部分,也是我向各位重点推荐先试的功能。它的核心逻辑是:输入数据库表名,读取表结构,根据字段名和数据类型自动推断出C#实体类、DTO、Service、Controller,以及LayUI列表页、表单页的HTML,最后生成到对应目录,重启项目就能用。
稍微解释下它如何推断字段用途:主键字段(如id)生成实体和编辑页时识别成隐藏字段;字段名包含time或类型为datetime的,列表页识别成时间列,表单页生成日期控件;字段类型为tinyint且值域有限时,生成下拉框选项。这套规则虽然简单,但覆盖了管理系统90%以上的字段场景。
要提醒的是,代码生成器生成的是"够用"的代码,不是"最好"的代码。建议生成后手动补充业务校验和关联查询逻辑。比如订单表生成出来后,金额字段的精度、状态流转的规则,这些业务语义还是需要人来处理。把它定位成"能帮你省掉重复代码的时间",而不是"替代你思考业务",用起来就非常舒服。
4. 实操过程与核心环节实现
4.1 环境准备与项目初始化
动手之前先准备好环境:Visual Studio 2022或Rider都行,SDK必须是.NET 8.0以上版本,数据库MySQL或SQLServer都可以,SqlSugar对两者的语法差异自动做了适配。
拿源码后第一步是改连接字符串,在appsettings.json里找到ConnectionStrings节点改成自己的库地址。第二步在项目根目录用命令初始化数据库结构:
dotnet run --project Xxx.Web --migrate这个自定义命令的作用是扫描所有实体类,调用SqlSugar的CodeFirst.InitTables按实体创建表。同时会检测数据字典表和系统配置表是否存在,没有就自动插入基础数据。我把这个过程拆解一下,方便你自己调整:它本质上就是一个IHostedService或命令行参数分支里的方法,在应用启动前判断环境变量,执行建表流程。
启动项目后访问http://localhost:5000进入登录页,默认管理员账号是admin,密码123456。登录后第一件事建议先去"系统管理-菜单管理"看看菜单数据结构,理解一下左侧菜单和数据库记录的对应关系,后面加新模块就知道要往哪插一条菜单记录了。
4.2 核心代码实现示例
新加一个"商品管理"模块来串一遍改造流程。首先在Xxx.Core里建一个实体类。
[SugarTable("product")] public class Product : EntityBase { [SugarColumn(ColumnName = "product_name", Length = 100)] public string ProductName { get; set; } [SugarColumn(ColumnName = "price", DecimalDigits = 2)] public decimal Price { get; set; } [SugarColumn(ColumnName = "stock")] public int Stock { get; set; } }EntityBase是框架提供的公共基类,里面已经包含了主键Id、CreateTime、UpdateTime这些通用字段。接着在Xxx.Application里建IProductService和ProductService,实现类的写法其实就是继承泛型基类服务:
public class ProductService : BaseService<Product>, IProductService { public ProductService(ISqlSugarClient db) : base(db) { } public async Task<bool> ReduceStockAsync(int productId, int count) { return await _db.Updateable<Product>() .SetColumns(p => p.Stock == p.Stock - count) .Where(p => p.Id == productId && p.Stock >= count) .ExecuteCommandAsync() > 0; } }注意ReduceStockAsync这个写法,把"库存扣减"和"库存充足校验"放在一条UPDATE语句的WHERE条件里完成,避免了并发下的超卖问题。这是个很小的技巧,但很多新手会先查库存再更新库存,两步之间可能被别的请求插进来,导致数据错乱。
控制器和前端页面这部分就不展开贴了,套路就是继承BaseController,方法上标注[Route("api/product")]和HTTP动词特性,返回ResultModel类型。列表页HTML拷贝另一个模块的页面改下表头字段即可,十分钟内一个单表模块就能端到端跑通。
4.3 功能测试与性能调优
把模块跑通之后,我建议按照这个顺序做一轮自测。先测接口:用Swagger逐个调用列表、详情、新增、修改、删除接口,确认每个接口的返回码和数据格式正确。Swagger在这个框架里配置了基础认证,所以测试前要先用管理员Token调用接口,要注意页面右上角Authorize按钮那里填Token的格式是Bearer xxxxx,漏掉Bearer前缀会一直报401,这是我最常看到的使用问题。
然后测权限:用一个普通只读角色的账号登录,访问新增和删除接口,确认后端真的拦住了没有权限的操作。前端按钮可能不显示了,但直接拿着普通账号的Token用工具调接口也应该被拒绝,后端权限这层一定要验。
性能方面,框架默认已经开启了响应压缩,主要调整空间在SqlSugar的SQL日志和连接池配置上。生产环境建议把连接字符串里的Min Pool Size设为2,Max Pool Size设为100,Connection Idle Timeout设30秒。高并发场景要特别关注查询是否走了索引,最简单的方法是在开发环境开启Aop.OnLogExecuting把每条SQL打印到控制台,看有没有全表扫描;框架默认打印所有执行SQL,生产环境需要关掉或者改成只记录慢SQL,能有效减少日志IO开销。
4.4 部署发布注意要点
部署这块踩过的坑不少,整理成清单给你参考。发布时用dotnet publish -c Release -o ./publish,然后整个publish目录拷贝到服务器。有几个生命周期管理的问题容易忽略:JWT签名的SecurityKey一定要改掉默认值,否则任何人都能伪造Token;数据库连接字符串和生产账号密码禁止明文提交到Git仓库,建议走环境变量或密钥管理服务。
Linux服务器上部署,用systemd做进程守护,配置Environment="ASPNETCORE_ENVIRONMENT=Production",反代用Nginx。反向代理要记得把X-Forwarded-For和X-Forwarded-Proto头传给应用,否则Swagger、回调URL、客户端IP获取都会有问题。LayUI的静态文件默认走wwwroot目录,发布时确认app.UseStaticFiles()在管道里的位置放在鉴权中间件之前,否则静态资源全被拦在后面。
5. 常见问题与排查技巧实录
5.1 SqlSugar相关的高频坑
第一个高频问题是"我的实体改了,但表结构没变"。原因通常是没跑建表命令,或者实体类缺少[SugarTable]特性导致映射不上。解决方式是确认实体类所在的程序集被启动项目扫描到了,SqlSugar默认只会在你传入的程序集里扫,全局搜下InitTables的调用代码就能找到扫描范围。第二个高频问题是"同一个上下文里查询出现脏数据",多见于把SqlSugarScope当成普通作用域对象用在并发场景。记住用框架默认的方式,单例注入、实例隔离,不要在业务代码里new SqlSugarClient。
还有关于事务的问题,多表操作用框架封装好的_db.Ado.UseTranAsync,不要手动开连接管事务,否则容易连接没释放导致池耗尽。使用事务时也尽量避免在事务内部调用外部HTTP接口,连接会把持有时间无限拉长,吞吐量下降特别明显。
5.2 LayUI表格与前端的坑
LayUI表格最常见的报错是"表格数据格式不正确"。后端返回格式必须是code、msg、count、data四个字段,code为0才算成功。很多新手直接在控制器里返回了一个List,前端解析不了就会报这个错。处理办法是框架里已经封装了ResultModel.Success()和PageResult<T>,统一用它们包返回。
另一个前端问题就是表格某一列添加点击事件,这个是问得最多的。LayUI的table模块本身没有直接在列配置里写事件回调的选项,正确做法是加一个操作列或者用templet模板自定义列内容,给元素打上自定义属性lay-event="edit",然后在table.on('tool(filter)'里监听:
table.on('tool(userTable)', function (obj) { var data = obj.data; if (obj.event === 'edit') { openEditDialog(data.id); } });这样做的好处是事件代理挂在了表格容器上,表格重载之后依然有效。如果直接把click事件绑在生成的元素上,表格刷新后事件就丢了,这是最常见的踩坑。
日期控件显示位置偏移的问题也有不少人遇到过,多半是弹窗或页面进行了滚动,日期面板绝对定位的基准没算对。解决办法是在打开layer.open时给area指定固定宽高,并设置scrollbar: false,或者把日期控件的position参数指到绑定元素上,实测下来基本能解决。
5.3 开发调试时的几个建议
调试阶段强烈建议开启SqlSugar的SQL日志输出,它能把每个查询的参数化SQL和耗时打出来,排查问题时价值巨大。框架里预留了Aop.OnLogExecuting的开关,开发环境用Serilog或直接Console.WriteLine输出,生产环境改造成记录到日志表里只记录超过500ms的慢SQL。
连接字符串的Allow User Variables和TreatTinyAsBoolean这两个参数,在MySQL下建议显式配置。尤其是TreatTinyAsBoolean,不配的话tinyint字段生成实体可能被映射成sbyte,取值0/1会变成-128/127这种怪值,很多数据错乱都是这么来的。SQLServer下没有这个问题。
5.4 接口安全与Swagger鉴权
部署到外网的框架,Swagger绝对不能裸奔。我在使用这个框架时给Swagger加了一层基础认证,实现方式是自定义一个IApiDescriptionGroupNameProvider或者直接在管道里加app.UseSwagger前先套一个判断请求头的中间件,只允许携带正确用户名密码的请求访问/swagger路径。还有一个更简单的做法是直接剥离Swagger的发布环境引用,只在Development环境注册Swagger中间件,这样生产环境根本没有这个入口,最省心。
接口层面全局统一做了参数绑定校验,[ApiController]特性自带的模型校验会在参数不合法时直接返回400。如果接口里接收了枚举值或日期字符串,一定注意前端传过来的格式要和模型绑定器对齐,否则返回的信息不够友好,排查起来也费劲。
写在最后的使用心得
老实说,这个框架并不是什么划时代的创新,它就是把多年做的管理系统里那些一遍遍重复的活儿沉淀了下来。我实际改造过一个十几张表的库存管理系统,原来预计三周的开发量用了不到一周就交付了,剩余时间全部花在了业务规则的打磨和测试上,这大概就是这类基础框架最大的价值——把人从无意义的重复代码里解放出来,去处理真正有挑战的业务问题。
使用过程中我最想强调的一点是,改造前先花半天时间完整读一遍现有模块的代码,尤其是登录流程和数据访问层,把框架的约定摸清楚再动手。很多人拿到项目第一件事就是业务代码,结果因为不熟悉基类方法或返回格式,后面越写越别扭。另外,生产环境上线前,务必把默认密码、JWT密钥、Swagger开关这三样东西全部处理掉,这是安全底线。
如果你正准备做一个C#.NET的管理系统,不妨拿这套框架试一把。就算最后不用它,跟着源码走一遍它的分层和封装思路,对理解ASP.NET 8.0下的MVC、API组合开发也有不小的帮助。后面我会在这个框架的基础上继续更新代码生成器的模板配置和更多业务场景的实现,有兴趣的读者也可以留言交流你在实际改造中遇到的问题,踩过的坑一起聊才更有意思。
本文还有配套的精品资源,点击获取