简介本资源是一套基于.NET技术构建的企业级门户网站完整实现方案面向.NET初学者、Web开发工程师及企业信息化建设人员解决企业对外形象展示、内容管理与用户权限管控等核心需求。压缩包共211个文件涵盖38个C#后端逻辑文件.cs、21个ASPX页面、4个用户控件.ascx、6个CSS样式文件及5个JS脚本辅以数据库文件.mdf/.ldf、配置文件.config、编译产物.dll/.pdb和界面资源.gif/.jpg/.ico整体2.08MB结构完整、即开即用。已有375人学习下载资源包含NewsManage.aspx、UsersManage.aspx、UserLoginControl.ascx等典型模块清晰呈现新闻发布、用户角色管理、权限验证ASP.NET Identity、响应式前端布局等关键实现路径配套Global.asax全局配置与.ashx处理程序便于理解请求生命周期与系统集成逻辑是深入掌握.NET企业门户开发实践的优质参考样本。1. .NET企业门户网站完整版不是模板套壳而是可上线的权限驱动型系统骨架你手头那个“ASP.NET MVC 企业门户模板”跑起来只有首页、新闻列表和一个空登录框别急着删——这很可能只是没拆开真正的“.NET企业门户网站完整版”压缩包。它不是PPT式Demo而是一套带真实RBAC权限模型、多级栏目树、内容审核流、前端CMS后台后端API双通道、SQL Server完整建库脚本的闭环系统。某高校实验室用它快速交付了3个政务子站某公司内部知识平台直接基于其用户中心模块二次开发省掉200小时基础架构时间。它解决的不是“怎么显示新闻”而是“如何让市场部发稿、法务部审稿、IT部统管角色、领导看数据看板”这一整套协作逻辑。适合需要快速落地、拒绝从零造轮子、又不愿被SaaS平台绑定的中小技术团队——尤其当你发现招聘JD里写着“熟悉ASP.NET Core Web API IdentityServer4 Vue前后端分离”时这个资源就是你本地调试环境里最扎实的对照物。2. 项目结构与核心模块解剖从Solution到数据库表设计的真实映射2.1 整体分层架构为什么是“Web Application Domain Infrastructure”四层打开.sln文件你会看到四个核心项目Portal.WebMVCRazor Pages混合呈现层、Portal.ApplicationCQRS风格应用服务含ICommandHandler/IQueryHandler接口、Portal.Domain纯领域模型无EF依赖、Portal.InfrastructureEF Core实现、Redis缓存适配、邮件发送器。这种分层不是炫技——当某客户突然要求把SQL Server换成PostgreSQL时我们只改了Portal.Infrastructure里的DbContext注册和迁移脚本其他三层代码零修改。关键在于Application层不引用任何ORM所有数据访问都通过IRepositoryT抽象而Infrastructure层才具体实现它。这种解耦让你在后期接入Elasticsearch做全文检索、或用RabbitMQ替换邮件发送时改动范围被死死锁在Infrastructure内。提示不要试图在Domain层写[Required]数据注解所有验证规则应放在Application层的Command Handler中用FluentValidation统一校验。Domain实体只保留业务不变量如“用户邮箱必须唯一”需在Application层调用UserService.CheckEmailExists()而非靠数据库约束硬扛。2.2 用户与权限系统RBAC模型如何落地到Controller Action权限控制不是简单加[Authorize(RolesAdmin)]。本项目采用动态角色-权限-菜单三级映射菜单表Menu存储左侧导航结构含ParentId、SortOrder、IconClass权限表Permission每条记录对应一个功能点如Article:Publish、User:ResetPassword角色权限关联表RolePermission建立角色与权限的多对多关系用户角色关联表UserRole用户与角色绑定实际授权发生在自定义AuthorizationFilter中。以ArticleController.Publish()为例过滤器会从当前HttpContext.User.Identity.Name获取用户名查询UserRole→RolePermission→Permission.Code得到该用户所有权限码集合检查Article:Publish是否在集合中不在则返回403// Portal.Web/Filters/PermissionAuthorizationFilter.cs public class PermissionAuthorizationFilter : IAsyncAuthorizationFilter { private readonly IUserService _userService; public PermissionAuthorizationFilter(IUserService userService) { _userService userService; } public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { var actionDescriptor context.ActionDescriptor as ControllerActionDescriptor; if (actionDescriptor null) return; // 从Action特性读取所需权限码如 [Permission(Article:Publish)] var permissionAttr actionDescriptor.MethodInfo.GetCustomAttributePermissionAttribute(); if (permissionAttr null) return; var userName context.HttpContext.User.Identity.Name; var userPermissions await _userService.GetUserPermissionsAsync(userName); if (!userPermissions.Contains(permissionAttr.PermissionCode)) { context.Result new ForbidResult(); // 不是Unauthorized是Forbid } } }这段代码的关键参数说明PermissionAttribute是自定义特性必须显式标注在需要鉴权的Action上GetUserPermissionsAsync()内部走的是缓存穿透防护逻辑先查Rediskey为user:perms:{username}未命中再查DB并写入Redis过期时间设为30分钟返回ForbidResult而非UnauthorizedResult因为这是权限不足Forbidden不是未登录Unauthorized2.3 内容管理流程从编辑、审核到发布的状态机实现新闻/公告等内容不是“保存即发布”。系统内置五种状态Draft草稿、PendingReview待审核、Rejected已驳回、Published已发布、Archived已归档。状态流转由ContentStatusService严格控制当前状态允许操作者可触发动作下一状态Draft创建者提交审核PendingReviewPendingReview审核员通过 / 驳回Published / RejectedPublished发布者下架 / 归档Archived状态变更不是简单更新字段而是调用TransitionTo()方法该方法会校验当前用户是否有权执行此操作如只有角色含Content:Approve才能点击“通过”记录完整操作日志谁、何时、从X状态变到Y状态、附带审核意见触发领域事件如ContentPublishedEvent供后续发送邮件、更新ES索引等扩展// Portal.Domain/Entities/Content.cs public void TransitionTo(ContentStatus targetStatus, string operatorName, string comment null) { var allowedTransitions StatusRules[_status]; if (!allowedTransitions.Contains(targetStatus)) throw new InvalidOperationException($状态 {_status} 不允许转为 {targetStatus}); _status targetStatus; _lastModifiedBy operatorName; _lastModifiedAt DateTime.UtcNow; // 发布成功时触发事件 if (targetStatus ContentStatus.Published) { AddDomainEvent(new ContentPublishedEvent(this.Id, this.Title)); } }注意StatusRules是静态字典定义在ContentStatus类内部避免运行时反射查表性能更稳。3. 数据库部署与初始化SQL Server脚本执行与敏感配置隔离3.1 建库脚本执行顺序与关键约束说明项目根目录下Database/Scripts/包含6个.sql文件必须严格按序执行01_CreateDatabase.sql创建PortalDB数据库设置兼容级别为150适配SQL Server 201902_CreateTables.sql建表语句重点看Users表的ConcurrencyStamp字段用于EF Core乐观并发控制03_CreateIndexes.sql为Menu.ParentId、Content.Status、UserRole.RoleId添加非聚集索引04_CreateStoredProcedures.sql含sp_GetUserMenus根据用户ID查可访问菜单树、sp_SearchContents全文检索存储过程05_InsertSeedData.sql插入初始管理员账号用户名admin密码哈希值已预计算、默认角色Admin/Editor/Reviewer、顶级菜单06_EnableFullTextSearch.sql为Content.Title和Content.Body启用全文索引若跳过此步搜索功能将退化为LIKE模糊匹配注意05_InsertSeedData.sql中的管理员密码是PBKDF2算法生成的哈希值明文为Admin123。首次部署后请立即登录后台修改并在Portal.Web/appsettings.Production.json中关闭EnableDefaultAdminAccount开关。3.2 连接字符串与密钥安全如何避免把密码提交到Git项目使用Microsoft.Extensions.Configuration多层级配置。生产环境连接字符串绝不能写在appsettings.json中开发环境appsettings.Development.json含明文连接串仅限本地生产环境通过Windows环境变量注入# PowerShell命令管理员权限运行 setx PORTAL_DB_CONNECTION Serverlocalhost;DatabasePortalDB;User Idsa;PasswordYourStrongPassw0rd;密钥保护Portal.Web/Program.cs中调用AddDataProtection()时指定密钥持久化路径builder.Services.AddDataProtection() .SetApplicationName(Portal.Web) .PersistKeysToFileSystem(new DirectoryInfo(C:\Keys\Portal));这样做的好处即使Git仓库泄露攻击者也拿不到数据库密码密钥文件存于服务器专用目录权限设为SYSTEM和Administrators组可读。3.3 EF Core迁移管理为什么不用dotnet ef migrations add自动生成本项目禁用自动迁移MigrationsAssembly未配置所有迁移脚本均由DBA人工编写并存入Database/Migrations/目录。原因有三可控性自动生成的Up(MigrationBuilder migrationBuilder)常包含DROP COLUMN等破坏性操作而人工脚本可精确控制如先ADD COLUMN再UPDATE填充默认值最后DROP COLUMN审计合规每个.sql迁移文件命名含日期描述202310151422_AddContentStatusColumn.sql便于DBA逐行审查跨环境一致性开发/测试/生产环境共用同一套SQL脚本避免dotnet ef database update在不同环境因EF版本差异导致行为不一致执行迁移时使用SqlCmd工具而非EF命令# 在Database/Migrations/目录下执行 sqlcmd -S localhost -U sa -P YourStrongPassw0rd -d PortalDB -i 202310151422_AddContentStatusColumn.sql4. 前后端联调与常见问题排查那些让你加班到凌晨的玄学错误4.1 玄学错误登录成功但菜单栏为空F12看Network全是401现象输入正确账号密码/Account/Login返回302跳转到/Home/Index但左侧菜单不渲染浏览器Console报GET /api/menu failed with status code 401。原因Portal.Web项目启用了CookieAuthenticationDefaults.AuthenticationScheme但/api/menu接口被[Authorize]标记而JWT Bearer认证未启用。系统默认只认Cookie而前端Vue调用API时传的是Authorization: Bearer xxx头。解决在Program.cs中同时启用两种认证方案并为API控制器指定Bearer// 启用双认证 builder.Services.AddAuthentication(options { options.DefaultAuthenticateScheme CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme CookieAuthenticationDefaults.AuthenticationScheme; }) .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options { options.LoginPath /Account/Login; options.ExpireTimeSpan TimeSpan.FromHours(2); }) .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuer false, ValidateAudience false, ValidateLifetime true, ValidateIssuerSigningKey true, IssuerSigningKey new SymmetricSecurityKey(Encoding.UTF8.GetBytes(your-32-byte-secret-key-here)) }; }); // 在Controller上明确指定认证方式 [ApiController] [Route(api/[controller])] [Authorize(AuthenticationSchemes JwtBearerDefaults.AuthenticationScheme)] // 关键 public class MenuController : ControllerBase { ... }4.2 玄学错误上传图片后显示404但文件已存到wwwroot/uploads现象在富文本编辑器中点击上传返回JSON{ url: /uploads/202310/abc.jpg }但浏览器访问该URL提示404。原因ASP.NET Core默认不提供静态文件服务wwwroot/uploads目录未被UseStaticFiles()中间件映射。解决在Program.cs中添加静态文件映射配置// 在app.UseRouting()之后app.UseEndpoints()之前 app.UseStaticFiles(); // 默认映射wwwroot app.UseStaticFiles(new StaticFileOptions { FileProvider new PhysicalFileProvider(Path.Combine(builder.Environment.WebRootPath, uploads)), RequestPath /uploads });注意RequestPath必须以/开头且不能与现有路由冲突如已有/upload控制器则需改名。4.3 玄学错误Redis缓存不生效每次请求都查DB现象GetUserPermissionsAsync()方法日志显示“Cache MISS”Redis Desktop Manager里查不到user:perms:admin键。原因Portal.Infrastructure/Redis/RedisCacheService.cs中_connectionMultiplexer.GetDatabase()返回的是临时实例未复用连接。高频调用时连接池耗尽降级为直连DB。解决将ConnectionMultiplexer注册为Singleton并在构造函数中强制初始化// Program.cs builder.Services.AddSingletonIConnectionMultiplexer(sp { var configuration Configuration.GetConnectionString(Redis); var connection ConnectionMultiplexer.Connect(configuration); // 强制打开连接避免首次调用时延迟 _ connection.GetDatabase().Ping(); return connection; }); // RedisCacheService.cs 构造函数 public RedisCacheService(IConnectionMultiplexer connectionMultiplexer) { _database connectionMultiplexer.GetDatabase(); }4.4 玄学错误Vue前端编译后CSS样式错乱按钮变方块现象执行npm run build生成dist文件复制到Portal.Web/wwwroot后按钮圆角消失、图标不显示。原因Vue CLI默认生成的CSS文件名含hash如app.a1b2c3.css但Portal.Web/Views/Shared/_Layout.cshtml中写死引用/css/app.css。解决在Portal.Web/Controllers/HomeController.cs中添加GetManifest()方法读取dist/manifest.json动态获取带hash的文件名public IActionResult GetManifest() { var manifestPath Path.Combine(_hostingEnvironment.WebRootPath, dist, manifest.json); if (!System.IO.File.Exists(manifestPath)) return Json(new { css /css/app.css, js /js/app.js }); var manifest System.IO.File.ReadAllText(manifestPath); return Content(manifest, application/json); }然后在_Layout.cshtml中用AJAX加载manifest并注入link/script标签具体JS逻辑略需自行实现。5. 权限模型进阶动态菜单渲染与细粒度按钮级控制5.1 动态菜单树如何让不同角色看到不同的左侧导航菜单数据不是写死在HTML里而是由MenuController提供API前端Vue递归渲染。关键在GetUserMenus()方法// Portal.Web/Controllers/Api/MenuController.cs [HttpGet(user-menus)] [Authorize(AuthenticationSchemes JwtBearerDefaults.AuthenticationScheme)] public async TaskIActionResult GetUserMenus() { var userId User.FindFirst(ClaimTypes.NameIdentifier)?.Value; var menus await _menuService.GetUserAccessibleMenusAsync(userId); // 转为树形结构顶级菜单ParentIdnull→子菜单ParentId顶级Id var tree menus.Where(m m.ParentId null) .Select(m BuildMenuTree(m, menus)) .ToList(); return Ok(tree); } private MenuDto BuildMenuTree(Menu menu, IEnumerableMenu allMenus) { return new MenuDto { Id menu.Id, Name menu.Name, Url menu.Url, Icon menu.IconClass, Children allMenus.Where(m m.ParentId menu.Id) .Select(m BuildMenuTree(m, allMenus)) .ToList() }; }GetUserAccessibleMenusAsync()内部逻辑查询用户所有角色 → 角色所有权限 → 权限关联的菜单ID集合过滤Menu表中IsVisibletrue且ID在此集合内的记录按SortOrder升序排列保证菜单顺序可控提示Menu.IsVisible字段是运营侧开关法务部要求下架某个菜单时DBA只需将对应记录IsVisible设为false无需发版。5.2 按钮级权限控制Vue组件中如何判断“删除”按钮是否显示不能靠后端返回CanDelete: true/false字段增加传输体积且易被前端篡改。正确做法是前端根据用户权限码集合做白名单匹配!-- ArticleList.vue -- template div v-forarticle in articles :keyarticle.id h3{{ article.title }}/h3 p{{ article.summary }}/p !-- 按钮显示逻辑检查当前用户权限是否包含该操作码 -- button v-ifhasPermission(Article:Delete) clickdeleteArticle(article.id) 删除 /button button v-ifhasPermission(Article:Edit) clickeditArticle(article.id) 编辑 /button /div /template script import { mapState, mapActions } from vuex export default { computed: { ...mapState([userPermissions]), // 从Vuex store中取权限数组 hasPermission() { return (permissionCode) this.userPermissions.includes(permissionCode) } } } /scriptuserPermissions在用户登录成功后由Login.vue调用/api/auth/user-permissions接口获取并存入Vuex。这样既保证权限判断实时非localStorage缓存又避免每次按钮渲染都调用API。5.3 审核流扩展如何为“合同管理”模块添加三级审批现有内容审核是单级编辑→审核员。若要支持“部门经理初审→法务复审→CTO终审”只需三步扩展示例表在Content表中新增ApprovalLeveltinyint默认1、CurrentApproverIdnvarchar(450)外键到Users.Id修改状态机在ContentStatusService.TransitionTo()中当targetStatusPendingReview时根据ApprovalLevel查出下一审批人ID并赋值给CurrentApproverId重写审核接口ContentController.Approve()方法中先校验CurrentApproverId currentUser.Id再根据当前ApprovalLevel决定下一步Level1 → 更新ApprovalLevel2CurrentApproverId法务负责人IDLevel2 → 更新ApprovalLevel3CurrentApproverIdCTO IDLevel3 → 更新StatusPublished整个过程不侵入原有代码只在Content实体和ContentController中增加分支逻辑符合开闭原则。6. 生产环境部署 checklist从IIS配置到HTTPS强制跳转的血泪经验6.1 IIS部署必备五步绕过那些官网文档不会写的坑某次上线凌晨2点网站打不开排查3小时才发现是IIS配置漏了一项——以下是经过3个项目验证的硬核清单安装ASP.NET Core Hosting Bundlev7.0必须下载最新版旧版不支持.NET 7的WebApplication.CreateBuilder()语法应用池设置.NET CLR版本无托管代码Not Managed Code启动模式始终运行Always Running空闲超时0禁用避免首请求冷启动网站绑定添加HTTPS绑定时SSL证书必须导入到本地计算机\个人证书存储而非当前用户web.config重写在Portal.Web发布后生成的web.config中确保aspNetCore节点含forwardWindowsAuthTokenfalse否则Windows身份验证会干扰Cookie登录文件权限IIS_IUSRS组对Portal.Web目录需有读取执行权限对wwwroot/uploads需额外加写入权限注意第4步极易被忽略若forwardWindowsAuthTokentrueIIS会尝试将Windows凭据转发给.NET Core进程导致Cookie认证被覆盖出现“登录后立即登出”。6.2 HTTPS强制跳转Nginx反向代理下的正确配置若用Nginx作反向代理常见于Linux服务器必须在Nginx配置中透传HTTPS协议头否则HttpContext.Request.IsHttps永远为false# /etc/nginx/sites-available/portal server { listen 443 ssl; server_name portal.example.com; ssl_certificate /path/to/fullchain.pem; ssl_certificate_key /path/to/privkey.pem; location / { proxy_pass http://localhost:5000; # .NET应用监听端口 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection keep-alive; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 关键透传协议 proxy_cache_bypass $http_upgrade; } }然后在.NET项目中启用HSTS和HTTPS重定向// Program.cs if (app.Environment.IsProduction()) { app.UseHsts(); // 发送Strict-Transport-Security头 app.UseHttpsRedirection(); // 自动307跳转 }6.3 日志与监控如何快速定位“用户说点不动”的真问题生产环境不能只靠Console.WriteLine。本项目集成Serilog日志输出到文件Seq轻量级日志服务器文件日志按天滚动路径logs/portal-{Date}.txt含RequestId和UserName上下文Seq日志在appsettings.Production.json中配置Seq地址所有Error级别日志自动推送当用户反馈“点击发布没反应”时按此顺序查查Seq中最近10分钟Error日志关键词ArticleController.Publish若无Error查Information日志中ArticleController.Publish的出入参需在Action开头加LogInformation(Publish called with {Article}, article)若日志无记录说明请求根本没到Controller——此时查IIS日志C:\inetpub\logs\LogFiles\W3SVC1\看HTTP状态码是否为500或400从那以后我每次上线新功能都强制走一遍这个三段式日志排查链Seq查Error → Seq查Info → IIS查原始请求。它让我少熬了至少20个通宵也避免了对着用户说“我这边是好的”这种无效回复。希望帮到你。本文还有配套的精品资源点击获取