ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

.NET Core 3.1跨平台云管理系统源码解析:多库适配、RBAC与部署

.NET Core 3.1跨平台云管理系统源码解析:多库适配、RBAC与部署 简介这是一套基于 .NET Core 3.1 开发的跨平台智能云管理系统源码面向 ASP.NET Core 开发者及企业内部系统建设者适合作为公司后台管理系统的快速开发脚手架。压缩包共 1433 个文件整体约 6.44MB其中 328 个 HTML、327 个 JS、209 个 CSS 文件用于前端展示206 个 C# 源码与 78 个 cshtml 视图组成后端逻辑与页面6 个 SQL 脚本用于数据库初始化另有 JSON 配置、Markdown 说明、Nginx/NLog 部署配置等目录结构分层清晰便于快速定位。目前已有 278 人学习下载。系统内置公司管理、用户管理、角色管理、部门管理、数据库管理、日志管理、菜单配置、服务器管理等核心模块并支持 SQL Server、MySQL、Oracle 三种数据库切换日志模块涵盖系统登录日志、接口调用日志等能力。压缩包内还附有发布、调试、启动脚本可帮助开发者快速理解跨平台部署、多数据库适配、日志记录的实现方式整体代码分层清晰模块边界明确直接基于该项目进行二次扩展。1. .NET Core 3.1 跨平台智能云管理系统为什么这套源码值得拆这套 .NET Core 3.1 跨平台智能云管理系统源码核心不是具体业务而是一整套能直接当底座的后台框架公司、部门、用户、角色、菜单、数据库、服务器、日志全带管理界面数据库在 SQL Server、MySQL、Oracle 三端由一套配置切换publish 脚本、nginx.conf、nlog.config 都放在根目录里。做中后台交付最关心的两件事——多租户隔离和 RBAC 权限链路——正好是这套框架的核心。适合接企业单、做后台框架选型、或正把 .NET Framework 老项目往 .NET Core 迁的开发者。下文按源码真实文件推进先看三库适配怎么做再拆构建脚本和权限模型最后落到 Nginx 部署与日志排查跑通一遍就能评估它适不适合当下个项目底座。2. 数据库三端适配EF Core 在 SQL Server、MySQL、Oracle 之间的切换实现2.1 先从 packages.config 和 bindings 判断依赖形态源码根目录里同时出现 packages.config 和 bindings 两个文件第一眼容易误会工程格式。packages.config 是 .NET Framework 时代 NuGet 的清单格式.NET Core 3.1 的标准做法是直接用 csproj 里的 PackageReference。这套源码里还留着 packages.config通常是两种情况一部分老类库仍然是非 SDK 风格工程或者是从旧方案迁移时遗留的清单文件。构建脚本能同时兼容这两种形态说明作者在依赖管理上刻意留了后手迁移中途换库不至于把旧项目卡死。bindings 文件一般对应 System.ServiceModel 相关的 WCF 绑定配置即老接口层曾经用 WCF 暴露过服务。把 bindings 和 packages.config 放在一起基本可以断定这套系统是从 .NET Framework 时代迁移过来的老服务用 WCF 绑定新 API 用 ASP.NET Core。迁移期如果要兼容老客户端传 XML 消息体Startup 里需要补上 XML 序列化支持写法如下services.AddControllers() .AddXmlDataContractSerializerFormatters() // 兼容 WCF/老客户端传 XML .AddNewtonsoftJson(); // 3.1 时代默认 JSON 行为挂载参数说明AddXmlDataContractSerializerFormatters 注册的是 DataContract 序列化器与 WCF 的契约模型同源老客户端按 DataContract 约定的消息体可以直接被新 API 反序列化AddNewtonsoftJson 负责 JSON 行为的兼容配置。两者共存的好处是同一套 Controller 同时收 XML 和 JSON不会直接回 415。如果这套源码里没有这两个调用说明迁移时已经砍掉了 XML 通道只留 JSON。2.2 DbContext 按配置动态切换 Provider多库支持不能靠改代码重编译常见做法是在 appsettings.json 里放一个 DbType 开关启动时由配置决定注册哪个数据库 Provider。核心代码如下public void ConfigureServices(IServiceCollection services) { var dbType Configuration[DbConfig:DbType]?.ToLower(); var connStr Configuration.GetConnectionString(Default); services.AddDbContextCloudDbContext(options { switch (dbType) { case sqlserver: options.UseSqlServer(connStr); break; case mysql: options.UseMySql(connStr, ServerVersion.AutoDetect(connStr)); break; case oracle: options.UseOracle(connStr); break; default: throw new NotSupportedException($不支持的数据库类型: {dbType}); } }); }这段代码的关键在 UseMySql 的第二个参数。Pomelo 3.1 版本要求显式传 ServerVersion否则启动时无法从连接串推断 MySQL 的主次版本ServerVersion.AutoDetect 会先连一次库取版本号代价是启动时间多出几百毫秒。Oracle 的 UseOracle 来自 Oracle 官方包 Oracle.EntityFrameworkCore如果 NuGet 还原时没有这个包优先检查是不是走了内网源或离线源。注意三库共用一个 DbContext 时迁移文件一定要按库分目录。否则一个模型改动会同时污染三个库的迁移历史评审时无从核对。切换数据库时迁移脚本不能混在一起我一般按库建立独立目录dotnet ef migrations add InitCloud --context CloudDbContext -o Migrations/SqlServer dotnet ef database update --context CloudDbContext切到 MySQL 或 Oracle 时把 -o 换成对应目录重新生成。CI 里按目标库执行对应目录的脚本模型变化产生的迁移文件互不覆盖。2.3 三库连接串与类型映射的差异点连接串长相差异很大源码里通常在 ConnectionStrings 节点下并列三份由 DbType 决定取用哪一份。三个库的连接串形态和坑位如下数据库Provider 包连接串示例端口容易踩的坑SQL ServerMicrosoft.EntityFrameworkCore.SqlServerServer127.0.0.1,1433;DatabaseCloudAdmin;User Idsa;Passwordxxx;1433地址和端口用逗号分隔分号只用来分隔键值MySQLPomelo.EntityFrameworkCore.MySqlServer127.0.0.1;Port3306;Databasecloudadmin;Userroot;Passwordxxx;3306库名表名在 Linux 下大小写敏感建议统一小写OracleOracle.EntityFrameworkCoreUser Idsystem;Passwordxxx;Data Source127.0.0.1:1521/XEPDB1;1521Data Source 是服务名不是 SID连错报 ORA-12514比连接串更麻烦的是类型映射。三库对同一份实体属性生成的列类型完全不同SQL Server 的 nvarchar(max) 在 MySQL 是 longtext在 Oracle 是 NCLOBDateTime 在 SQL Server 默认 datetime2MySQL 是 datetime(6)Oracle 是 TIMESTAMPbool 在 SQL Server 是 bitMySQL 是 tinyint(1)Oracle 是 NUMBER(1)。如果三套库共用同一份实体定义OnModelCreating 里必须按 Provider 区分protected override void OnModelCreating(ModelBuilder builder) { if (Database.IsSqlServer()) { builder.EntityApiLog().Property(l l.Message).HasColumnType(nvarchar(max)); } else if (Database.IsMySql()) { builder.EntityApiLog().Property(l l.Message).HasColumnType(longtext); } else if (Database.IsOracle()) { builder.EntityApiLog().Property(l l.Message).HasColumnType(NCLOB); } }主键策略也是重灾区。三库对自增的支持口径不一致SQL Server 用 IDENTITYMySQL 用 AUTO_INCREMENTOracle 12c 之后的版本虽然有 IDENTITY但老实例还得靠 SEQUENCE。所以跨三库的框架主键常见选择是 Guid由代码端赋值避免依赖任何一端的自增行为排序需求则附加 CreateTime 字段。这套源码能在三库间切换基本就是靠这类统一约定兜底。3. 构建脚本与本地调试publish.bat、dotnet_run.bat、bundleconfig.json 的工程化细节三个脚本的分工可以用一张表说清脚本输出配置输出目录典型用途publish-debug.batDebugpublish\debug测试环境出包publish-release.batReleasepublish\release生产环境出包dotnet_run.batDevelopment无产物本地直接起服务Debug 与 Release 脚本差异极小核心只是 -c 参数不同但正是这个参数决定 JIT 优化、调试符号和日志级别。Debug 包自带 pdb 和更详细的运行时输出适合内网联调Release 包才有完整 JIT 优化生产环境不要用 Debug。3.1 publish-debug.bat 与 publish-release.bat 的参数分工这两个脚本可以理解为 CI 之前的最后一道人工关卡。Debug 脚本给测试环境出包Release 脚本用于正式发布差异只集中在 -c 参数和输出目录echo off setlocal set ROOT%~dp0 set CSPROJ%ROOT%src\Cloud.Admin.Web\Cloud.Admin.Web.csproj set OUTPUT%ROOT%publish\debug echo [1/3] restore... dotnet restore %CSPROJ% echo [2/3] publish... dotnet publish %CSPROJ% -c Debug -o %OUTPUT% --no-restore echo done: %OUTPUT% pauseRelease 脚本只要把 -c Debug 换成 -c Release输出目录改成 publish\release。这里 -o 指定的是输出根目录不是文件--no-restore 表示跳过还原依赖 restore 在第一步完成出多环境包时能省几十秒。如果要打 Linux 部署包需要在发布脚本里加运行时标识dotnet publish src/Cloud.Admin.Web/Cloud.Admin.Web.csproj -c Release -r linux-x64 --self-contained false -o publish/linux-r linux-x64 指定目标运行时生成 Linux 版宿主程序--self-contained false 表示依赖目标机器上已安装的 .NET Core 3.1 运行时包体小、更新快。如果目标机器不允许装运行时则去掉这个参数打成自包含包体积增加约 60MB但免安装。源码用 VS2019 打开时这些 bat 不参与解决方案构建只有右键项目选发布或手工双击才生效。所以不要把 bat 直接拖进 CI 任务里跑CI 里用 dotnet publish 原生命令更可控出错时日志也更好定位。3.2 dotnet_run.bat本地调试的环境变量注入dotnet_run.bat 是给开发者在 VS2019 外快速启动服务的入口核心不是 dotnet run 那一行而是前面两行 setecho off set ASPNETCORE_ENVIRONMENTDevelopment set ASPNETCORE_URLShttp://0.0.0.0:5000 dotnet run --project src\Cloud.Admin.Web\Cloud.Admin.Web.csprojASPNETCORE_ENVIRONMENTDevelopment 会触发 appsettings.Development.json 的加载这个文件里通常覆盖了本地库连接串、日志级别和 JWT 密钥不设这个变量时默认环境是 Production启动后很可能直接连上生产库后果很严重。ASPNETCORE_URLS 覆盖 Kestrel 监听地址写成 0.0.0.0:5000 是为了让局域网内其他机器访问方便前端联调自己单机调试时我习惯改成 127.0.0.1:5000少开一个对外端口少一份风险。dotnet run --project 后面必须跟 csproj 路径在根目录直接敲 dotnet run 会尝试从当前目录找工程文件找不到就报 MSB1003。如果机器上装了多个 SDK脚本顶部最好加一行 dotnet --version 注释避免 3.1 工程被新 SDK 的隐式行为干扰。3.3 bundleconfig.json静态资源的打包与压缩入口bundleconfig.json 是 BuildBundlerMinifier 的配置文件VS2019 里保存或重新生成解决方案时会按它合并压缩静态资源。典型内容如下[ { outputFileName: wwwroot/dist/app.min.css, inputFiles: [ wwwroot/css/bootstrap.css, wwwroot/css/font-awesome.css, wwwroot/css/admin.css ] }, { outputFileName: wwwroot/dist/app.min.js, inputFiles: [ wwwroot/js/jquery.min.js, wwwroot/js/common.js, wwwroot/js/menu.js ] } ]outputFileName 是合并后的产物路径inputFiles 按数组顺序合并数组的先后就是最终文件里的代码顺序依赖关系必须靠这个顺序保证jquery 不放在数组第一个后面脚本里 $ 调用全部报错。这个配置只在本地构建时生效Linux 发布包里不会执行 VS 的打包任务所以 CI 里不需要压缩时把 _Layout.cshtml 的引用改成未压缩版本即可必须压缩的话要在 csproj 里保留 BuildBundlerMinifier 包引用并在发布前用 dotnet build 触发一次打包任务。4. 核心模块拆解公司隔离、RBAC 权限链路与双日志体系4.1 公司管理多租户隔离的全局查询过滤器公司管理模块在云管理系统里承担的是多租户边界不是单纯的通讯录维护。Sys_Company 一行代表一个租户部门、用户、角色、菜单配置全挂在 CompanyId 上。数据隔离靠 EF Core 的全局查询过滤器比在每个查询里手动加条件可靠得多protected override void OnModelCreating(ModelBuilder builder) { var tenantId _currentTenantId; builder.EntityDepartment().HasQueryFilter(d d.CompanyId tenantId); builder.EntityUser().HasQueryFilter(u u.CompanyId tenantId); builder.EntityRole().HasQueryFilter(r r.CompanyId tenantId); }_currentTenantId 从哪来登录成功后 JWT 的 Claims 里带 CompanyId程序里通过 IHttpContextAccessor 取当前请求的 Claims赋值给 DbContext 的实例属性。DbContext 默认是 Scoped每个请求创建一次所以过滤器拿到的租户 ID 就是当前请求的租户不会串。两个要留意的边界一是全局过滤器对 Include 和导航属性同样生效但前提是关联实体也注册了过滤器二是超级管理员账号通常没有 CompanyId 或为 0这类账号的查询过滤器要显式放开。提示有些私有化部署直接把租户 ID 做成配置项写死等于放弃多租户能力只适合单公司场景。谈多租户交付时这条必须问清楚。顺带说一个同类框架里常见的结构通用 CRUD 抽成 BaseService 和 BaseRepository业务层继承后免费获得增删改查和分页。这套系统的公司、部门、用户模块基本就是这个三层实例二次开发新增业务表时继承 BaseRepository 就能省掉绝大部分重复代码主键、分页、排序、软删除都走统一入口。4.2 用户、角色、部门、菜单后台权限链路的表设计与查询RBAC 的实体与关系可以归成一张表模块数据表关系说明公司管理Sys_Company顶层租户部门管理Sys_DepartmentDepartmentId 挂在用户上用户管理Sys_User、Sys_UserRole用户与角色多对多角色管理Sys_Role、Sys_RoleMenu角色与菜单多对多菜单配置Sys_MenuParentId 组成树形结构登录成功的用户能看哪些菜单本质是两条多对多中间表的 JOIN用户先通过 Sys_UserRole 拿到角色集合角色再通过 Sys_RoleMenu 拿到菜单集合最终返回按 Sort 排序的树形菜单SELECT DISTINCT m.MenuId, m.MenuName, m.Url, m.ParentId, m.Sort FROM Sys_Menu m INNER JOIN Sys_RoleMenu rm ON rm.MenuId m.MenuId -- 角色与菜单中间表 INNER JOIN Sys_UserRole ur ON ur.RoleId rm.RoleId -- 用户与角色中间表 WHERE ur.UserId UserId AND m.MenuType page AND m.IsEnable 1 ORDER BY m.Sort;这条 SQL 的要点在 DISTINCT一个用户挂多个角色时不同角色会重复挂同一个菜单DISTINCT 保证前端渲染菜单不出现重复节点。菜单配置管理模块维护的就是 Sys_Menu 表ParentId 为 0 是一级菜单叶子节点通常是页面 URL 或按钮权限码。按钮级权限不在这条 SQL 里常见做法是 Sys_Menu 每行带 PermissionCode前端渲染按钮时用它做过滤后端在 Controller 的 Attribute 里做二次校验。只做前端隐藏不做后端校验权限等于摆设这是这类系统二次开发时最容易埋雷的地方。4.3 数据库管理与服务器管理两个被低估的高危模块数据库管理模块通常提供两类能力一是维护系统要连的其他业务库的连接信息二是提供一个受限的 SQL 执行界面。连接信息存 Sys_Database 表执行时按 DbType 动态创建连接public IDbConnection CreateConnection(string dbType, string connStr) { switch (dbType.ToLower()) { case sqlserver: return new SqlConnection(connStr); case mysql: return new MySqlConnection(connStr); case oracle: return new OracleConnection(connStr); default: throw new NotSupportedException($不支持的数据库类型: {dbType}); } }SqlConnection、MySqlConnection、OracleConnection 三个类型来自不同包但都实现 IDbConnection上层代码不用区分统一 Open / ExecuteReader 即可。这个模块权限必须收口生产环境默认只允许系统管理员角色打开每次执行把 SQL 原文、执行人、耗时写入 Sys_DatabaseLog否则等于给所有登录用户开了一个数据库后门。服务器管理模块在 .NET Core 3.1 下要注意告别 WMI 依赖。老项目用 System.Management 拿 CPU 和内存这套跨平台源码在 Linux 上要走 DriveInfo 和 /proc/meminfo 这类系统 API或者引入跨平台诊断包。只做界面展示不在生产上开放远程命令执行这是底线。4.4 登录日志与接口日志落库与落盘的分工日志体系分两层登录日志要求可回查落库接口日志追求全量落盘。登录日志在登录 Action 里按结果写 Sys_LoginLog记录用户名、IP、浏览器、登录时间、成败结果。真实 IP 的处理留到第 5 章讲Nginx 转发后 RemoteIpAddress 会变成 127.0.0.1不处理就误判全部来自本机。接口日志用中间件拦截全量请求。中间件里要读请求体必须先 EnableBuffering否则 Body 流被消费完Controller 里 ModelBinding 拿到空参数public async Task InvokeAsync(HttpContext context) { var sw Stopwatch.StartNew(); context.Request.EnableBuffering(); using (var reader new StreamReader(context.Request.Body, Encoding.UTF8, leaveOpen: true)) { var body await reader.ReadToEndAsync(); context.Request.Body.Position 0; // 复位保证后续模型绑定能读到 _logger.LogInformation(接口: {Method} {Path} 参数{Body}, context.Request.Method, context.Request.Path, body); } await _next(context); sw.Stop(); _logger.LogInformation(接口: {Path} 状态{Status} 耗时{Ms}ms, context.Request.Path, context.Response.StatusCode, sw.ElapsedMilliseconds); }EnableBuffering 把 Body 换成可重复读的缓冲流Position 0 复位后 ModelBinding 才能正常取值leaveOpen: true 保证 reader 释放时不关闭底层流。耗时采集放在 _next 之后得到的是包含整个请求管线的总耗时落盘时和状态码一起写排查慢接口直接按耗时排序就行。这一层只做 NLog 文本写盘不做业务结构化避免中间件自身出错拖垮主流程。5. 日志与 Nginx 部署从 nlog.config 到反向代理的最后一公里5.1 nginx.conf 反向代理配置源码根目录预置的 nginx.conf说明作者默认的生产拓扑是 Nginx 把 80 端口请求转发给本机 Kestrel。最小可用配置如下server { listen 80; server_name admin.example.com; location / { proxy_pass http://127.0.0.1:5000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 透传客户端真实 IP proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 20m; } }proxy_pass 指向 Kestrel 的 localhost 地址Kestrel 端监听 127.0.0.1:5000 就够不必对公网开放。X-Forwarded-For 是登录日志和接口日志拿到真实 IP 的前提程序侧还要配合启用转发头处理services.ConfigureForwardedHeadersOptions(options { options.ForwardedHeaders ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; }); app.UseForwardedHeaders();注意 UseForwardedHeaders 要放在 UseAuthentication 之前否则认证流程先取到 127.0.0.1登录日志里的来源 IP 全部失真。5.2 nlog.config 的路径与滚动策略nlog.config 默认按天写文件发布到 Linux 后最常出的问题就是日志目录没有写权限nlog xmlnshttp://www.nlog-project.org/schemas/NLog.xsd targets target namefile xsi:typeFile fileName${basedir}/logs/${shortdate}.log layout${longdate}|${level:uppercasetrue}|${logger}|${message} ${exception:formattostring} archiveEveryMonth maxArchiveFiles6 / /targets rules logger name* minlevelInfo writeTofile / /rules /nlog${basedir} 是程序运行目录发布包里 logs 文件夹要先建好并授权给运行账号写权限systemd 服务里用 Userwww-data 统一管理比较省心。archiveEvery 按月归档并保留 6 份避免单目录无限膨胀。nlog.config 必须设置复制到输出目录否则 Linux 上启动后完全没日志排查时无从下手。5.3 部署验证与权限变更后的缓存坑发布完成、Nginx reload 之后验证链路按这个顺序走curl -I http://127.0.0.1:5000/api/health curl -H Host: admin.example.com http://127.0.0.1:5000/api/user/page | jq .code tail -f /var/www/cloud/logs/$(date %F).log第一条确认 Kestrel 存活第二条带 Host 头走完整转发链路第三条看真实请求是否落到 NLog 文件。压轴提醒角色和菜单权限改完后经常出现菜单没变、接口 403 的怪问题多半是 JWT 里的角色或权限 Claims 还是旧值。这类框架的登录态默认不主动刷新改完权限让用户重新登录是成本最低的解法如果要求无感刷新就把角色信息从 JWT 迁到服务端缓存并加版本号每次请求比对版本不一致时强制重签令牌。查到这一步这套跨平台云管理系统的部署链路基本就闭环了。本文还有配套的精品资源点击获取
返回列表