基于ADMIN.NET快速搭建.NET后台管理系统:从环境配置到业务模块开发实战

基于ADMIN.NET快速搭建.NET后台管理系统:从环境配置到业务模块开发实战
1. 项目概述为什么选择 ADMIN.NET最近在规划一个内部使用的业务中台需要快速搭建一套包含用户、角色、权限、菜单等基础功能的后台管理系统。自己从零开始造轮子显然不现实时间成本和维护成本都太高。在.NET生态里找了一圈最终把目光锁定在了ADMIN.NET这个项目上。它不是一个新名词而是基于当下非常流行的Furion框架和SqlSugarORM 构建的一套开箱即用的通用管理平台脚手架。简单来说ADMIN.NET 就像是一个为你准备好的、精装修的“毛坯房”。它已经帮你把地基Furion框架、承重墙权限体系、水电管线基础数据管理都搭建好了你只需要根据自己的业务需求在预留好的空间里进行“软装”开发业务模块即可。这对于需要快速启动项目、又希望保持代码规范和架构清晰的团队或个人开发者来说吸引力巨大。尤其是它底层采用的 SqlSugar以其简单易用、性能不俗的特性在.NET社区积累了大量的开发者基础降低了学习和使用的门槛。我选择上手 ADMIN.NET核心目标很明确第一验证其作为生产项目基石的稳定性和扩展性第二梳理出一套清晰、可复用的开发流程和最佳实践第三记录下从环境搭建到第一个业务模块开发全过程中遇到的“坑”和解决方案。这篇笔记就是这次探索之旅的起点我会尽量用直白的语言分享从零到一的实操过程。2. 环境准备与项目初探2.1 开发环境搭建工欲善其事必先利其器。ADMIN.NET 基于 .NET 6/8因此第一步是确保本地开发环境就绪。安装 .NET SDK前往微软官网下载并安装最新稳定版的 .NET SDK建议直接上 .NET 8。安装完成后在命令行执行dotnet --version确认安装成功。数据库准备ADMIN.NET 默认支持 SqlServer、MySQL、PostgreSQL等多种数据库。我以最常用的MySQL 8.0为例。你需要在本机或远程服务器上安装好MySQL并创建一个空的数据库例如admin_net_db。记住连接字符串的格式后面会用到。代码编辑器/IDE毫无疑问Visual Studio 2022是首选它对 .NET 6/8 和新 C# 特性的支持最为完善。也可以使用 JetBrains Rider 或 VS Code根据个人习惯选择。获取源码访问 ADMIN.NET 的 GitHub 仓库或 Gitee 镜像将项目克隆到本地。通常项目结构会比较清晰包含后端Admin.NET.Web.Core、前端可能基于 Vue3/Element Plus等。注意在拉取代码时请关注项目的README.md和wiki确认其依赖的 Furion 和 SqlSugar 的具体版本。不同版本间可能存在细微差异最好与项目推荐版本保持一致避免不必要的兼容性问题。2.2 项目结构与核心依赖解析将项目拉取到本地并用 VS2022 打开后我们先来快速浏览一下它的解决方案结构。一个典型的 ADMIN.NET 后端项目可能包含以下几个核心层Admin.NET.Web.Core应用启动层包含Program.cs、Startup.cs或仅Program.cs对于 .NET 6、配置文件等。Admin.NET.Application应用服务层这里放置你的业务逻辑代码是开发者的主战场。Admin.NET.Core核心实体层定义数据库实体模型Entity、数据传输对象DTO、以及一些核心的通用类。Admin.NET.EntityFramework.Core数据访问层基于 SqlSugar 的仓储Repository实现和数据库上下文配置。Admin.NET.Database.Migrations数据库迁移项目用于管理数据库结构的变更。在Admin.NET.Web.Core项目的appsettings.json配置文件中你需要重点关注数据库连接字符串的配置。找到ConnectionStrings节点将其修改为你自己的数据库信息。{ ConnectionStrings: { DefaultConnection: Serverlocalhost;Port3306;Databaseadmin_net_db;Uidroot;Pwdyour_password;Charsetutf8mb4; } }接下来打开项目的 NuGet 包管理器你会看到核心依赖Furion提供了一整套 Web 应用开发的基础设施如依赖注入、动态 WebAPI、规范化结果、对象映射等。ADMIN.NET 的权限验证、日志、缓存等模块很可能深度集成了 Furion 的能力。SqlSugarCoreORM 框架负责所有数据库操作。它的特点是语法糖多链式调用流畅学习成本低。理解这个结构至关重要因为它决定了你后续代码应该写在哪个项目里。简单规则实体定义在 Core业务逻辑在 Application数据库访问通过 Application 调用 EntityFramework 层提供的仓储接口。3. 数据库初始化与首次运行3.1 运行数据库迁移ADMIN.NET 通常使用 Code First 模式。这意味着你不需要手动执行 SQL 脚本来创建表而是通过运行迁移命令让 EF Core 或 SqlSugar 根据你的实体模型自动生成数据库表。设置启动项目在解决方案资源管理器中右键将Admin.NET.Web.Core设置为启动项目。配置数据库连接确保上一步的appsettings.json中的连接字符串正确无误。生成迁移并更新数据库打开程序包管理器控制台工具 - NuGet 包管理器 - 程序包管理器控制台。在默认项目下拉框中选择你的数据库迁移项目例如Admin.NET.Database.Migrations。输入命令Add-Migration InitialCreate来创建一个名为 “InitialCreate” 的迁移。这会在 Migrations 文件夹下生成一个包含本次模型变更的 C# 文件。然后输入命令Update-Database将迁移应用到数据库。执行成功后打开你的 MySQL 客户端应该能看到数据库里生成了几十张表包括sys_user、sys_role、sys_menu、sys_org等系统基础表。实操心得第一次迁移时如果遇到类似“无法更新数据库因为存在未应用的迁移”或版本冲突错误可以尝试先执行Remove-Migration移除上次失败的迁移或者直接删除 Migrations 文件夹后重来。更稳妥的做法是在空数据库上操作。3.2 启动项目与登录验证数据库准备好后直接按 F5 运行Admin.NET.Web.Core项目。项目启动后控制台会输出 Swagger 文档的访问地址通常是https://localhost:port/swagger/index.html。访问 Swagger在浏览器中打开 Swagger 地址。这里你会看到 ADMIN.NET 自动生成的所有 API 接口文档非常清晰。这也是测试接口的好工具。寻找登录接口在 Swagger 中找到认证相关的接口通常是/api/auth/login。ADMIN.NET 默认会初始化一个超级管理员账号查阅项目文档或种子数据文件可能在Admin.NET.Core的SeedData文件夹下可以找到默认账号密码常见的是admin / 123456。获取 Token使用默认账号调用登录接口成功后返回的响应体中会包含一个accessToken字段。复制这个 Token 值。授权访问在 Swagger 页面的顶部找到一个 “Authorize” 或锁形图标点击它在弹出的对话框中输入Bearer 你的Token注意 Bearer 后面有个空格。这样你就可以在 Swagger 中测试其他需要权限的接口了比如获取用户列表、菜单列表等。如果前端项目是分离的通常也需要启动前端服务如npm run dev并在前端登录页面使用相同账号登录。成功登录并看到管理后台的主界面意味着你的基础环境已经跑通了4. 核心模块解析以用户管理为例要真正上手最好的方式就是解剖一个现有功能。我们以最核心的系统用户管理模块为例看看 ADMIN.NET 是如何组织代码的。4.1 后端代码结构追踪在Admin.NET.Application项目中寻找与用户相关的服务。你可能会找到一个名为SysUserService的类。这个类继承了某个基础应用服务类并实现了ISysUserService接口。这是典型的基于接口的依赖注入模式也是 Furion 推荐的做法。打开SysUserService你会看到诸如GetPageList、AddUser、UpdateUser、DeleteUser等方法。我们以分页查询用户列表GetPageList为例输入参数方法通常接收一个UserInput类型的参数这是一个封装了查询条件如用户名、手机号、机构ID和分页参数页码、页大小的 DTO定义在Admin.NET.Core层。仓储调用在方法内部会通过依赖注入的IRepositorySysUser或类似的泛型仓储来访问数据库。这里就是 SqlSugar 发挥作用的起点。服务层会构建一个ISugarQueryableSysUser的查询对象并根据输入参数动态添加Where条件。数据返回调用仓储的AsQueryable()、WhereIF()、ToPagedListAsync()等方法最终返回一个PagedListSysUserOutput。SysUserOutput是另一个 DTO用于向前端返回数据它可能只包含需要展示的字段并做了关联查询如关联角色名、机构名。// 伪代码示例展示服务层逻辑 public async TaskPagedListSysUserOutput GetPageList(UserInput input) { // 1. 构建查询 var query _userRep.AsQueryable() .WhereIF(!string.IsNullOrWhiteSpace(input.Account), u u.Account.Contains(input.Account)) .WhereIF(input.OrgId 0, u u.OrgId input.OrgId) .LeftJoinSysOrg((u, o) u.OrgId o.Id) // 关联机构表 .Select((u, o) new SysUserOutput { Id u.Id, Account u.Account, RealName u.RealName, OrgName o.Name // 关联字段 }); // 2. 分页查询 var pagedList await query.ToPagedListAsync(input.PageNo, input.PageSize); return pagedList; }4.2 前端页面与API对接转到前端项目假设是 Vue3在src/api目录下找到user.js或类似的 API 封装文件。这里定义了调用后端接口的方法。// 前端 API 调用示例 import request from /utils/request export function getUserList(params) { return request({ url: /api/sysUser/page, method: get, params }) }在用户管理的 Vue 组件中会在created或mounted生命周期或者通过点击查询按钮调用getUserList方法将页面上的查询表单数据作为参数传入。获取到后端返回的分页数据后再赋值给表格的>using SqlSugar; namespace Admin.NET.Core.Entity; [SugarTable(net_product)] // 指定表名 public class Product : EntityBase { /// summary /// 产品名称 /// /summary [SugarColumn(Length 100)] public string Name { get; set; } /// summary /// 产品编码 /// /summary [SugarColumn(Length 50, IsNullable true)] public string Code { get; set; } /// summary /// 价格 /// /summary [SugarColumn(ColumnDataType decimal(18,2))] public decimal Price { get; set; } /// summary /// 库存 /// /summary public int Stock { get; set; } /// summary /// 产品状态0-下架1-上架 /// /summary public int Status { get; set; } /// summary /// 产品描述 /// /summary [SugarColumn(ColumnDataType text, IsNullable true)] public string Description { get; set; } }EntityBase已经包含了主键 Id通常是 long 或 snowflake id、创建信息等我们只需定义业务字段。[SugarColumn]特性用于配置字段属性如长度、是否可为空、数据库类型等。5.2 创建应用服务与接口在Admin.NET.Application项目中新建一个IProductService接口和ProductService类。接口定义 (IProductService.cs)定义服务契约。public interface IProductService { TaskPagedListProductOutput GetPageList(ProductInput input); Tasklong AddProduct(AddProductInput input); Task UpdateProduct(UpdateProductInput input); Task DeleteProduct(DeleteProductInput input); TaskProductOutput GetDetail(long id); }服务实现 (ProductService.cs)实现业务逻辑。这里需要注入泛型仓储IRepositoryProduct。public class ProductService : IProductService { private readonly IRepositoryProduct _productRep; public ProductService(IRepositoryProduct productRep) { _productRep productRep; } public async TaskPagedListProductOutput GetPageList(ProductInput input) { var query _productRep.AsQueryable() .WhereIF(!string.IsNullOrWhiteSpace(input.Name), p p.Name.Contains(input.Name)) .WhereIF(input.Status.HasValue, p p.Status input.Status.Value) .OrderBy(p p.CreateTime, OrderByType.Desc) .Select(p new ProductOutput { Id p.Id, Name p.Name, Code p.Code, Price p.Price, Stock p.Stock, Status p.Status, CreateTime p.CreateTime }); return await query.ToPagedListAsync(input.PageNo, input.PageSize); } // ... 实现其他增删改查方法 }ProductInput、ProductOutput、AddProductInput等 DTO 需要在Admin.NET.Core的相应文件夹下定义。5.3 注册服务与生成数据库表服务注册在Admin.NET.Web.Core项目的Program.cs或某个模块化的服务配置文件中添加services.AddScopedIProductService, ProductService();。生成迁移再次打开程序包管理器控制台在迁移项目下执行Add-Migration AddProductTable。更新数据库执行Update-Database。此时数据库会新增net_product表。创建控制器在Admin.NET.Web.Core的控制器文件夹下新建ProductController继承 Furion 的DynamicApiController它就能自动将ProductService中的方法暴露为 Web API。[ApiDescriptionSettings(产品管理)] public class ProductController : DynamicApiController { private readonly IProductService _productService; public ProductController(IProductService productService) { _productService productService; } [HttpGet(/api/product/page)] public async TaskPagedListProductOutput GetPageList([FromQuery] ProductInput input) { return await _productService.GetPageList(input); } // ... 其他 Action 对应 Service 方法 }重启后端项目访问 Swagger你应该能看到新增的/api/product/page等接口。至此一个完整的后端 API 模块就开发完成了。前端页面的开发则是类似的套路创建路由、页面组件、调用 API、绑定数据。6. 深入踩坑与最佳实践在实际操作中肯定会遇到一些问题。这里记录几个我遇到的典型问题及其解决方案。6.1 SqlSugar 时间回退问题处理这是一个从网络热词里看到的具体问题“sqlsugar 服务器时间出现回退 ]处理让他不报错返回新id”。这通常发生在使用 SqlSugar 的Insertable(entity).ExecuteReturnSnowflakeId()方法时如果服务器时间发生回退比如手动调整了系统时间或从 NTP 服务器同步时间时出现跳变基于雪花算法生成 ID 的机器可能会产生重复或冲突的 ID导致插入失败。解决方案禁用雪花ID对于不严格要求全局唯一递增、数据量不大的表可以在实体主键 Id 上使用自增[SugarColumn(IsIdentity true, IsPrimaryKey true)]或者使用 GUID。使用 SqlSugar 的容错配置SqlSugar 提供了应对时钟回拨的策略。可以在项目启动时配置 SqlSugar。services.AddSqlSugar(new ConnectionConfig() { ConnectionString config[ConnectionStrings:DefaultConnection], DbType DbType.MySql, IsAutoCloseConnection true, ConfigureExternalServices new ConfigureExternalServices { EntityService (property, column) { // 配置雪花算法的工作机器ID和回拨容忍 if (column.IsPrimarykey property.PropertyType typeof(long)) { column.IsSnowflake true; // 设置工作机器ID分布式环境下需区分 column.SnowflakeIdWorker new SnowflakeIdWorker(1, 1); // 启用时钟回拨容忍关键配置 column.SnowflakeEnableBackTime true; } } } });SnowflakeEnableBackTime true这个配置是关键它允许在发生微小的时间回退时算法等待时间追上来而不是直接抛出异常。但对于大的时间回退仍需从运维层面保证服务器时钟同步。自定义 ID 生成器如果业务允许可以自己实现一个不依赖严格时间戳的分布式 ID 生成方案。6.2 权限配置与菜单管理ADMIN.NET 自带完善的权限体系。当你新增了一个ProductController如何让它受权限控制并出现在管理菜单里权限编码在控制器或 Action 上使用[Permission]特性。ADMIN.NET 通常有一套自动扫描并收集权限码的机制。你需要查阅项目文档看它是如何定义权限码格式的例如product:page:query。[HttpGet(/api/product/page)] [Permission(product:page:query)] public async TaskPagedListProductOutput GetPageList(...)同步权限项目启动时或通过某个管理功能将标注了[Permission]的接口同步到系统的sys_menu或sys_permission表中。配置菜单登录后台管理系统在“系统管理”-“菜单管理”中新增一个菜单。填写菜单名称、路由地址对应前端路由、组件路径、图标等。关键一步在“权限标识”字段填入上一步定义的权限码如product:page:query。这样这个菜单项就会根据当前用户的角色和权限动态显示或隐藏。角色授权在“角色管理”中为你测试的角色分配刚添加的“产品管理”菜单权限。6.3 事务管理与单元工作在涉及多个数据库操作如同时插入订单和扣减库存的业务中必须使用事务保证数据一致性。ADMIN.NET 结合 Furion 和 SqlSugar可以很方便地使用事务。推荐做法在应用服务层的方法上使用[UnitOfWork]特性。Furion 的工作单元UnitOfWork会自动管理事务边界。[UnitOfWork] public async Task CreateOrder(CreateOrderInput input) { // 1. 创建订单 var orderId await _orderRep.InsertReturnSnowflakeIdAsync(new Order {...}); // 2. 扣减库存假设有产品ID和数量 await _productRep.UpdateAsync(p new Product{Stock p.Stock - input.Quantity}, p p.Id input.ProductId); // 如果任何一步失败整个方法内的操作都会回滚 }也可以使用 SqlSugar 的ITenant或SqlSugarScope来手动控制事务但[UnitOfWork]在大多数场景下更简洁且能与 Furion 的异常处理、仓储模式更好地结合。6.4 前端开发注意事项如果 ADMIN.NET 提供了前端项目它很可能基于 Vue3 TypeScript Vite Element Plus。开发新模块时API 封装统一遵循项目已有的utils/request.ts拦截器模式统一处理请求头如添加 Token、响应错误等。状态管理了解项目使用的是 Pinia 还是 Vuex按照其模式定义和管理产品模块的状态。组件复用充分利用项目已有的表格、表单、弹窗等基础组件保持 UI 风格一致。路由配置在路由文件中添加新产品页面的路由通常需要配置元信息meta如title、icon以便与后端菜单的权限标识关联。7. 性能调优与扩展思考当业务模块逐渐增多数据量变大后一些性能问题就需要提前考虑。SqlSugar 查询优化**避免 SELECT ***在服务层.Select()时只查询需要的字段。合理使用索引为经常作为查询条件的字段如Name,Status,CreateTime建立数据库索引。分页务必加 OrderBy分页查询时一定要有明确的排序条件否则在不同数据库或不同时间查询结果顺序可能不一致。大数据量导出对于导出功能不要一次性查询全部数据到内存。应使用 SqlSugar 的ToDataTablePageAsync或流式查询分批次处理。缓存策略ADMIN.NET 可能集成了内存缓存或分布式缓存如 Redis。对于一些不常变的基础数据如字典项、系统配置可以在服务层使用缓存。Furion 提供了[Cacheable]特性可以方便地实现方法结果缓存。[Cacheable(Key product:list:all)] public async TaskListProductOutput GetAllProducts() { return await _productRep.AsQueryable().ToListAsync(); }记得在数据更新时使用[CacheEvict]清除缓存。项目结构扩展当Application项目变得庞大时可以按业务模块拆分成多个子项目如Product.Application、Order.Application。考虑引入领域驱动设计DDD的一些概念如聚合根、领域服务来更好地组织复杂业务逻辑。上手 ADMIN.NET 只是一个开始。它提供了一个坚实、规范的起点但如何在这个基础上构建出健壮、易维护、高性能的业务系统还需要我们在理解其设计思想的基础上结合具体的业务场景不断地实践和优化。这套框架最大的价值在于其“约定大于配置”的理念和清晰的层次划分让开发者能更专注于业务逻辑本身而不是重复的基础设施编码。在后续的笔记中我会继续深入分享在权限深度定制、工作流集成、微服务化改造等方面的实践经验。