资讯动态

.NET Core3.1后台管理系统:EF Core迁移与LayUI前端实战拆解

发布时间:2026/9/13 13:38:30 来源:尧图企业网站定制
简介这套后台管理系统基于 .NET Core 3.1 与 EF Core 构建前端页面采用 LayUI 组件库整体遵循 MVC 分层设计适合初学 C# 企业级开发的程序员也适合需要快速搭建内部管理平台的中小型团队参考。压缩包内共 386 个文件大小约 4.69MB核心文件包括 C# 源码、Razor 视图、脚本与样式文件、JSON 配置及数据库备份并附带迁移脚本、项目解决方案以及界面演示动图目录结构清晰便于按模块查阅。目前已有 867 人学习下载。源码中包含数据库上下文、认证信息服务、基础服务、字符串扩展、枚举处理等关键类覆盖了依赖注入、通用增删改查、权限校验、Swagger 接口注释等常见后台开发需求。通过其中的数据库备份可快速还原演示环境并基于现有框架扩展业务模块无论用于学习架构还是作为项目初始模板都较为合适。1. 这套 .Net Core3.1 后台管理系统里最值得拆走的是什么拿到这个压缩包先别急着跑dotnet restore里面几个文件名已经暴露了底细Student.bak是数据库备份20200818064956_InitialCreate.cs是 EF Core 迁移文件BaseService.cs和AuthInfoService.cs是写业务逻辑的地基。这套基于 .Net Core3.1 EF Core LayUI 封装的 MVC 版后台管理系统并不是那种生成完代码就跑不起来的半成品而是把数据访问、登录会话、前端表格和接口文档串成了一条完整链路。适合刚开始做企业后台的人也适合想从三层架构里提炼一套通用 Service 的熟手。下面我会按数据层、Service 层、前端交互和验证几个环节来拆每个环节都会给出可以直接用在你项目里的写法。2. EF Core 迁移与数据库初始化从备份文件到 Code First 模型2.1 迁移快照与备份文件必须配对myDbContextModelSnapshot.cs记录的是当前DbContext的完整模型状态20200818064956_InitialCreate.cs是第一次生成迁移时的历史文件。这两个文件在 EF Core 里配合使用每次执行dotnet ef migrations addEF 先读取快照对比模型差异生成新迁移再更新快照。如果你已经用Student.bak还原了数据库项目里的迁移历史还停留在初始版本后续再执行迁移命令EF Core 就会尝试生成重复的表结构变更轻则提示模型不一致重则直接覆盖已有列。所以拿到这套代码第一件事不是去改实体类而是先决定是还原备份还是从迁移脚本重建库。2.2 用 RESTORE 还原 Student.bak还原备份是保留基础数据最直接的方式尤其当你还需要EnumNation这类字典数据时备份文件比空白迁移更省事。在 SQL Server 环境中最常用的命令是-- 先看备份内包含的逻辑文件确认 MOVE 目标 RESTORE HEADERONLY FROM DISK N/backup/Student.bak; RESTORE DATABASE Student FROM DISK N/backup/Student.bak WITH REPLACE, MOVE Student TO N/var/opt/mssql/data/Student.mdf, MOVE Student_log TO N/var/opt/mssql/data/Student_log.ldf;第一次执行时通常会在RESTORE HEADERONLY和RESTORE DATABASE之间来回改一下路径。REPLACE表示忽略现有同名数据库直接覆盖这在本地开发环境里没关系生产环境一定要小心MOVE ... TO ...把备份里的逻辑文件名映射到当前机器的物理路径如果两个路径不一致还原会失败并提示逻辑文件名不匹配。还原完成后还需要确认连接字符串里的初始目录是Student否则DbContext会连到 master。2.3 完全走迁移命令的替代方案如果不需要备份里的历史数据更干净的做法是直接让 EF Core 从零创建数据库。控制台执行dotnet tool install --global dotnet-ef --version 3.1.32 dotnet ef migrations add InitialCreate \ --project src/App.Data --startup-project src/App.Web dotnet ef database update \ --project src/App.Data --startup-project src/App.Web这里必须指定--version 3.1.32因为 .NET SDK 如果装了更高版本的dotnet-ef生成的迁移文件是 EF Core 5/6 的格式放进 3.1 项目里直接编译不过。--project指向包含DbContext的类库--startup-project指向 MVC 启动项目EF Core 会借助启动项目的配置读取连接字符串。当前目录最好也切到解决方案根目录否则相对路径容易找错。两种初始化方式的取舍我整理成了表格方便直接按自己的场景选场景推荐方式理由有历史字典数据、只求快速跑起来还原 Student.bak省去手动补基础数据需要干净结构、准备写自动化测试迁移命令数据库可重复创建多人协作、频繁改实体迁移命令迁移历史可版本管理给客户演示、不想暴露历史数据迁移命令备份文件可能包含敏感数据这个表格也能解释为什么很多老项目宁可保留一条备份链路因为迁移脚本只能保证结构一致不带业务初始化数据。2.4 表结构设计与 EnumNation 的存法还原或迁移成功之后看一下EnumNation.cs和Student实体就能理解这套系统的编码思路。民族字段没有用字符串而是用 int 存枚举值public class Student { public int Id { get; set; } public string Name { get; set; } public int Nation { get; set; } // 对应 EnumNation 的值 public DateTime Birthday { get; set; } public bool IsActive { get; set; } } public enum EnumNation { 汉族 1, 满族 2, 回族 3, 其他 99 }把枚举存成 int 的好处是前端下拉框和后端数据都只需处理数字LayUI 的 select 直接绑定数字Swagger 文档里也能清楚看到取值范围。缺点是可读性差所以项目里通常会配合StringExtensions.cs或一个枚举扩展方法把 1 转成“汉族”避免在 Controller 里到处写 switch。这种“int 枚举 展示扩展”的组合在 .Net Core3.1 的 MVC 后台系统里很常见既照顾了数据库索引效率又不牺牲 API 的可读性。3. BaseService 泛型封装与依赖注入把 CRUD 收进基类3.1 一个可上生产的 BaseService 骨架企业内部后台的 CRUD 页面80% 都长一个样增删改查、分页列表。如果每个模块都去写自己的_studentService、_classServiceController 会越来越臃肿。所以这套项目里直接把BaseServiceT抽出来了关键的几个方法如下public class BaseServiceT where T : class, new() { private readonly MyDbContext _db; public BaseService(MyDbContext db) { _db db; } // 按主键取实体优先从 EF Core 跟踪器里找 public T GetById(int id) { return _db.SetT().Find(id); } // 只读查询不挂跟踪减少 SaveChanges 时发生冲突的概率 public ListT GetList(FuncT, bool predicate) { return _db.SetT().AsNoTracking().Where(predicate).ToList(); } // 新增和修改统一走 SaveChanges public int Add(T entity) { _db.SetT().Add(entity); return _db.SaveChanges(); } public int Update(T entity) { _db.SetT().Update(entity); return _db.SaveChanges(); } public int Delete(int id) { var entity GetById(id); if (entity null) return 0; _db.SetT().Remove(entity); return _db.SaveChanges(); } }这段代码里有两个容易被忽略的细节。第一GetById用的是Find而不是FirstOrDefaultFind会先在DbContext的跟踪器里查如果之前查询过同一主键就直接返回省一次数据库访问但代价是它拿到的可能不是数据库最新值长时间保持同一个DbContext时要注意。第二GetList用了AsNoTracking()这是给列表查询和导出这类只读场景准备的如果后续要对返回的实体做更新不能直接指向这个集合否则需要重新附加。3.2 按命名约定批量注册 Service项目里的ServiceCollectionExtensions.cs就是用来做依赖注入扩展的。里面有很多类似AuthInfoService的类如果每个都在Startup.cs里services.AddScopedIXxx, Xxx()改动会很频繁。可以借助一次程序集扫描把服务注册收敛起来public static IServiceCollection AddAppServices(this IServiceCollection services) { var assembly Assembly.GetExecutingAssembly(); var serviceTypes assembly.GetTypes() .Where(t t.IsClass !t.IsAbstract t.Name.EndsWith(Service)) .ToList(); foreach (var impl in serviceTypes) { // 约定实现类叫 AuthInfoService接口叫 IAuthInfoService var serviceInterface impl.GetInterfaces() .FirstOrDefault(i i.Name I impl.Name); if (serviceInterface ! null) { services.AddScoped(serviceInterface, impl); } else { services.AddScoped(impl); } } return services; }在Startup.cs里调用一行services.AddAppServices()即可。这种约定式注册有两个前提一是服务类名必须统一以Service结尾接口名必须把I放在类名前面二是不能有同名接口在不同命名空间否则GetInterface会返回第一个。如果你喜欢显式注册也可以不用这段扫描但之后每加一个服务类就要改两行重复度高。下面是我整理的选择依据注册方式维护成本启动可控性适合场景手动 AddScoped高每加一个类改一次高一眼看清所有绑定小项目、类不超过10个约定扫描注册低加类即用低需要靠命名约定保证中后台、持续新增模块3.3 AuthInfoService 如何读取登录用户后台系统肯定要拿当前登录人。AuthInfoService通常不是认证处理器而是对HttpContext的用户信息做一层包装让控制器和 Service 层都能安全拿到用户 ID。代码里可以这样实现public class AuthInfoService { private readonly IHttpContextAccessor _httpContextAccessor; public AuthInfoService(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor httpContextAccessor; } public int? GetCurrentUserId() { var claim _httpContextAccessor.HttpContext? .User?.FindFirst(ClaimTypes.NameIdentifier); return int.TryParse(claim?.Value, out var id) ? id : null; } }注意IHttpContextAccessor是 ASP.NET Core 提供的服务直接注入会拿到单例对象但HttpContext本身跟请求上下文绑定所以不会串请求。需要在Startup.cs里显式加上services.AddHttpContextAccessor()否则构造注入会报错。拿到Claim之后业务代码里就可以直接判断当前用户对某个菜单是否有权限不用再各自写一套读取逻辑。3.4 升级到 .NET 8 时 BaseRepository 的演进思路到了 .NET 8 EF Core 8 阶段很多人会把BaseServiceT拆成BaseRepositoryT和BaseServiceT两层。Repository 层只负责IQueryableT和事务Service 层负责业务规则Controller 只依赖 Service。这样改动的目的是把 EF Core 的语法糖隔离在数据访问层方便以后做多租户切换或单元测试时替换内存数据库。如果你现在用的是这套 3.1 代码我建议先把BaseServiceT里的_db.SetT()全部换到IBaseRepositoryTController 不要直接注入DbContext。这个改造过程不复杂但可以避免以后直接从 3.1 跳到 8.0 时遇到AsNoTracking、Include和导航属性行为变化带来的大范围调整。4. LayUI 表格、下拉框与 MVC 路由的约定4.1 引入 LayUI 的静态资源顺序LayUI 跟 Bootstrap 这类框架不同它需要先加载layui.css再引入layui.js之后借助layui.use初始化模块。很多人把layui.use写在layui.js之前导致layui is not defined。link href~/lib/layui/css/layui.css relstylesheet / script src~/lib/layui/layui.js/script script layui.use([table, form, laydate], function () { const layer layui.layer; const form layui.form; const table layui.table; table.render({ elem: #studentTable, url: /Student/List, page: true, cols: [[{ field: name, title: 姓名 }]], done: function () { console.log(表格加载完成); } }); }); /script注意layui.use的回调是异步的所以表格、下拉框、日期控件都需要放在回调里。如果 MVC 布局页有多个子页面都调用 layui最好在每个子页面单独layui.use而不是在全局布局里一次初始化避免页面RenderSection(Scripts)被重复加载。4.2 layui table 请求 MVC Action 的返回结构LayUI 的table.render会自动向url发 GET 请求并且期望接口返回一个 JSON 对象而不是纯数组。这个对象必须包含code、msg、count和data四个字段否则表格会提示“接口异常”。在 MVC 的 Controller 里常见的写法是[HttpGet] public async TaskIActionResult List(int page 1, int limit 10) { var query _studentService.GetList(x x.IsActive); var total query.Count; var rows query .Skip((page - 1) * limit) .Take(limit) .ToList(); return Json(new { code 0, msg ok, count total, data rows }); }在 .Net Core3.1 的 MVC 里Json()方法默认使用 System.Text.Json 序列化日期格式会输出为 ISO 8601 字符串比如2020-08-18T00:00:00。LayUI 表格模板里需要处理T字符否则页面显示不友好。另外上表中的page和limit参数名是 LayUI 约定的如果改成了index、size还要在table.render里借助request重新映射。下表是 LayUI 标准字段对照返回字段说明常见错误code0 表示成功非0为异常直接返回了数据数组msg提示信息字符串忘了给字段count总记录数用于分页用了默认值100data当前页数据列表返回被包装了其他字段4.3 layui select 动态赋值form.val 和原生 value后台编辑页经常会拿到Student数据后动态设置下拉框常见有两种做法。一种是直接对整个表单赋值另一种是单独 set 某个元素的值。// 方式一一次给整个 form 填充 form.val(editForm, { Nation: data.nation, Status: data.status }); // 方式二单独赋值后重新渲染 $(#Nation).val(data.nation); form.render(select);两种方式的差别在于form.val会一次性处理表单里所有元素并且内部自动触发render方式二如果最后少写了form.render(select)页面上的显示值不会更新但隐藏的input已经变了提交时会出现页面看到的值和实际提交的值不一致。我一般会在编辑 Modal 弹出后优先使用form.val因为它能避免漏掉同一表单里多个 select 的渲染。4.4 laydate 最大日期限制日期控件laydate的max参数在后台表单里经常用来限制“出生日期不能大于当天”。代码很简单laydate.render({ elem: #Birthday, max: 0 });max: 0表示日期最大为今天也可以直接写max: 2025-12-31。要注意max必须是 YYYY-MM-DD 格式的字符串或数字如果传new Date()对象laydate 内部不会自动格式化控件仍然可以选择未来日期。这个细节在 .Net Core 后端和 LayUI 3.x 版本里都一样。4.5 MVC 路由与防伪令牌在 ASP.NET MVC 设计模式里路由是控制器的入口。这套项目一般在Startup.cs里使用传统的MapControllerRouteapp.UseEndpoints(endpoints { endpoints.MapControllerRoute( name: default, pattern: {controllerHome}/{actionIndex}/{id?}); });对于 POST 请求MVC 默认会校验ValidateAntiForgeryTokenAjax 提交时必须带防伪令牌。我一般会在页面上放一个Html.AntiForgeryToken()然后在脚本里读取__RequestVerificationToken字段发送出去$.ajax({ url: /Student/Save, method: POST, data: { Name: $(#Name).val(), Nation: $(#Nation).val(), __RequestVerificationToken: $(input[name__RequestVerificationToken]).val() }, success: function (res) { if (res.code 0) layer.msg(保存成功); } });如果使用 fetch 发送需要在 headers 中设置RequestVerificationToken后端也会接受。很多新人在本地可以跑通部署到 IIS 后因为虚拟目录名导致路由 404所以先确认pattern里的{id?}是否符合你的 URL 规划。5. 用 Swagger 过滤器与 Seed 数据完成链路验收5.1 DescriptionDocumentFilter 让接口文档可读DescriptionDocumentFilter.cs这个文件是 Swagger 文档过滤器我一开始以为只是改标题后来发现它还能给控制器和 Action 补说明。最简单的用法public class DescriptionDocumentFilter : IDocumentFilter { public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { swaggerDoc.Info.Description 后台管理系统 API 文档; swaggerDoc.Info.Title Admin API; } }在Startup.cs里配置AddSwaggerGen时要同时打开 XML 注释文件这样控制器里的///注释才能显示出来services.AddSwaggerGen(c { c.SwaggerDoc(v1, new OpenApiInfo { Title Admin API, Version v1 }); var filePath Path.Combine(AppContext.BaseDirectory, ${Assembly.GetExecutingAssembly().GetName().Name}.xml); c.IncludeXmlComments(filePath); c.DocumentFilterDescriptionDocumentFilter(); });这个过滤器适合放全局信息比如版本号、许可说明不要放每个接口的细节细节应该写在 Action 的 XML 注释里。5.2 启动时自动生成 Seed 数据Student.bak里已经有数据但如果你走的是迁移方式库里没有任何基础数据启动时可以先判断再 Seed。在Program.cs的Main里using (var scope host.Services.CreateScope()) { var db scope.ServiceProvider.GetRequiredServiceMyDbContext(); db.Database.Migrate(); if (!db.SetStudent().Any()) { db.Add(new Student { Name 张三, Nation 1, Birthday new DateTime(2000, 1, 1) }); db.Add(new Student { Name 李四, Nation 2, Birthday new DateTime(1999, 2, 3) }); db.SaveChanges(); } }db.Database.Migrate()会执行所有已经生成的迁移如果之前的迁移脚本和模型不一致这一步会提前暴露比在浏览器里报错容易排查。5.3 用 curl 验证 CRUD 与 LayUI 约定启动项目后先用 curl 验证最关键的列表接口curl -X GET http://localhost:5000/Student/List?page1limit10 \ -H Accept: application/json \ -H X-Requested-With: XMLHttpRequest期望返回code0data里有两条 Seed 数据count2。接着打开 Swagger 的 UI 页面在文档里直接点击ListAction 的执行按钮如果返回正常说明从 EF Core 到 BaseService 再到 MVC Controller 的整条链路是通的。前台 LayUI 表格能正常渲染再把 4.2 里的data字段格式和 4.3 的form.val核对一下。这个接口返回的 JSON 里日期还是2020-08-18T00:00:00的格式LayUI 的templet模板里要记得截取.split(T)[0]否则页面上会出现多余字符。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价