ARTICLE DETAIL

资讯详情

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

Asp.net Core + Vue前后端分离ERP系统部署与权限实现

Asp.net Core + Vue前后端分离ERP系统部署与权限实现 简介基于Asp.net Core与Vue前后端分离架构的ERP管理系统完整源代码主要面向.NET全栈开发者和企业信息化学习者适合用来理解权限控制、菜单管理、物料管理等核心功能在真实业务场景中的落地方式。压缩包整体约534KB共收录349个文件其中149个cs文件对应后端控制器、仓储、服务及实体映射37个vue文件组成前端页面组件39个js和77个css负责页面逻辑与视觉样式配合少量配置文件构成一套可运行的前后端分离工程。从内容预览可以看到项目内置SeedData种子数据初始化、RepositoryBase与IRepositoryBase仓储基类以及MenuService、MaterialService等业务服务有助于读者掌握接口封装、依赖注入以及EF Core数据持久化的常规写法。资源还包含log4net.config日志配置及数据库迁移初始化相关代码更方便在本地还原运行环境后直接调试。目前已有58人学习下载适合有一定C#基础、希望借助完整企业级案例熟悉前后端分离协作流程的开发者阅读并二次扩展。1. 为什么 ERP 要选 Asp.net Core Vue 的前后端分离方式很多企业级管理系统尤其 ERP都是从单体 ASP.NET WebForms 或者 MVC 演化过来的。这个 zip 包名称里的关键字Asp.net Core、Vue、前后分离代表的是目前企业信息化里比较常见的一套组合后端用 C# 写业务接口前端用 Vue 做单页应用二者通过 HTTP JSON 通信。拆到这种程度好处是 ERP 里像库存、采购、销售这些模块可以分别排期迭代后端加接口不影响前端发布前端换交互也不等后端重新编译。但代价也清楚项目变成两个工程环境配置、请求转发、部署方式都要交代清楚。下面按从后端到前端、从本地到服务器的顺序展开核心是把这套项目从解压变成一个能登录进主界面的系统。2. 从 zip 到可运行先理清 Asp.net Core 后端的启动路径2.1 先看目录结构确认是 API 还是老 MVC解压后不要急着用 Visual Studio 双击解决方案文件先用命令行看一眼工程形态。一般这种项目的目录会分成 Backend 和 Frontend 两个一级目录后端叫 ERP.Api、ERP.WebApi 或直接叫 Backend。执行tree -L 3 -I node_modules输出大致是这个样子Backend/ ├── ERP.sln ├── ERP.Api/ │ ├── Controllers/ │ │ ├── AuthController.cs │ │ ├── StockController.cs │ │ └── SaleController.cs │ ├── Program.cs │ ├── appsettings.json │ ├── appsettings.Development.json │ ├── ERP.Api.csproj │ └── wwwroot/看到 Controllers 和 Program.cs基本能判断这是 ASP.NET Core Web API 项目而不是老的 ASP.NET MVC 那种返回 cshtml 页面渲染的方式。Program.cs 是总入口负责构建 WebApplication、注册服务和中间件管道Controllers 下的一组类文件里每个 Controller 对应一类 ERP 业务资源比如 StockController 管库存出入库SaleController 管销售订单。这里有个容易忽略的位置项目经常用解决方案引用多个子项目比如 ERP.Domain、ERP.EntityFrameworkCore。启动项目必须是带Microsoft.NET.Sdk.Web的 API 项目不是 Domain 也不是 Application否则dotnet run会报找不到启动项目。判断依据是打开 csproj 看 Sdk 属性写Microsoft.NET.Sdk.Web的才是 Web 宿主。2.2 改连接串和 JWT 配置再执行 dotnet run先打开 appsettings.json核心配置就两个部分连接字符串和 JWT。ERP 的数据绝大多数存在 SQL Server 里默认连接串通常是指向本机开发库{ ConnectionStrings: { Default: Server.;DatabaseERP_DB;User Idsa;PasswordYourPassword;TrustServerCertificateTrue }, Jwt: { Issuer: ERP.Api, Audience: ERP.Client, Secret: Replace-With-At-Least-32-Chars-Random-String } }参数说明Server.是本地默认 SQL Server 实例命名实例要写成Serverhost\\instance如果数据库用 Windows 身份验证把User Id和Password换成Trusted_ConnectionTrue。TrustServerCertificateTrue适合开发环境避免本地自签名证书的链校验问题生产环境建议用正式证书或EncryptFalse并明确网络边界。JWT 里的 Secret 是给 token 签名的关键材料不能沿用项目里的示例值改成至少 32 位的随机字符串。Issuer 和 Audience 主要供后端的 TokenValidationParameters 校验使用字段存在即可不要删。然后执行命令cd Backend/ERP.Api dotnet restore dotnet run启动后控制台会打印Now listening on: http://localhost:5000这个端口来自 launchSettings.json 的 applicationUrl后面配 Vue 的转发目标时 must 与之完全一致。端口 5000 被占用时最常见是之前一次dotnet run没有退出或者 IIS Express 还挂着进程改端口或杀掉占用进程都行不要在代码里写死。2.3 后端跑不起来先查这三个位置下面是后端启动阶段最高频的几类报错按实际项目出现的频率排出来现象原因处理方式连接数据库报 401 或超时登录名密码错误、SQL Server 未启动先用 SSMS 或 sqlcmd 用同一账号连接确认调用接口全部 401 UnauthorizedJWT 配置被环境变量或 Development 配置覆盖检查 appsettings.Development.json 和系统环境变量启动报 No service for type DbContextAddDbContext 未注册或注册顺序不对在 Program.cs 中查找 AddDbContext 调用并确认已执行接口返回 404路由前缀与前端预期不一致查看 Controller 上的 Route 特性一般是 [Route(api/[controller])]后端日志默认不会打印 SQLERP 里常见的数据对不上问题比如成本数据跑不通多是库存单据和物料档案两边单位不一致导致。这时把日志级别临时调到 Information看 EF Core 生成的 SQL 模板只能确认执行成功业务数据对不对要看接口层的返回内容。后端能从启动到登录接口通整个项目就完成了三分之一。3. Vue 前端依赖安装与请求链路打通3.1 进入 Frontend 目录用 npm install 装依赖Vue 工程在项目里通常叫 Frontend 或 Web。不要用 Visual Studio 打开它用 VS Code 跑更贴近 Vue 的工具链。先确认 Node 版本再装依赖cd Frontend node -v npm config get registry npm install npm run serveVue CLI 项目在 Node 16 左右可以跑Vite 项目建议 Node 18 以上。npm config get registry如果返回官方源在公司网络环境里下载依赖很容易超时可以临时切到国内镜像源npm config set registry https://registry.npmmirror.com装完要不要换回全看个人习惯。npm install 报错集中在版本锁不一致比如 package-lock.json 引用了某个已被删除的版本。此时不要着急删 node_modules优先用npm ci它会按 lock 文件严格重建依赖树npm ci也失败再把报错信息定位到具体包判断是不是该调整包的版本区间。常用命令可以记一下命令使用场景注意事项npm install初次安装或 package.json 有变更可能改写 package-lock.jsonnpm ci严格复现锁文件会清空 node_modules 后重新安装npm run serve本地开发端口冲突时看控制台提示的替代端口npm run serve启动后开发服务器一般不直接访问后端而是通过 devServer 把/api开头的请求转发到 Asp.net Core 的地址这样前端开发时不需要关心 CORS。3.2 设计 axios 实例并配置请求转发目标前后分离项目里前端请求不能散落在各个页面要收敛到一个 axios 实例统一处理 token、错误提示和 401 跳转。src/utils/request.js 的常见写法import axios from axios const service axios.create({ baseURL: process.env.VUE_APP_API_BASE_URL || /api, timeout: 15000 }) service.interceptors.request.use(config { const token localStorage.getItem(erp_token) if (token) { config.headers.Authorization Bearer token } return config }) service.interceptors.response.use( response response.data, error { const status error.response error.response.status if (status 401) { localStorage.removeItem(erp_token) window.location.href /login } return Promise.reject(error) } ) export default servicebaseURL 优先读环境变量开发环境不配就统一走/api。请求拦截器在每次请求前把 JWT 拼到 Authorization 头后端[Authorize]标识的接口才能认出来。响应拦截器里遇到 401说明 token 过期或非法把本地 token 清掉再跳回登录页这是 Vue 前后端分离请求 token 处理的标准做法。在 vue.config.js 里配置转发规则注意这段只在开发模式生效module.exports { devServer: { proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } } }target 地址必须与后端dotnet run打印的监听地址一样changeOrigin 置 true 是为了改写请求头中的 Host。后端 Controller 前缀不是/api而是/erp时这里的路径也要同步改。经常有人直接照搬别人配置导致页面能开、接口全部 404。如果项目已经切到 Vite配置写在 vite.config.js 的 server.proxy 下写法基本一致。3.3 路由守卫负责拦住未登录的访客有 token 不等于有全部权限但至少能确认会话状态。Vue Router 的前置守卫写起来不长三个分支要清楚import router from ./router router.beforeEach((to, from, next) { const token localStorage.getItem(erp_token) if (to.meta.public) { next() return } if (!token) { next(/login) return } next() })登录页路由要标记meta: { public: true }否则用户在未登录状态访问 /login 也会被弹走形成跳转死循环。这一步只解决“能不能进系统”按钮权限和动态菜单留在后面处理。4. 把认证和菜单权限做成后端可控的 ERP 集成方案4.1 四张核心表用户、角色、菜单、用户角色ERP 权限最常见的模型是 RBAC后端不直接给前端控制每一行代码只维护用户、角色、菜单三组关系。数据库设计精简版如下在 SQL Server 里可以直接建CREATE TABLE Sys_User ( Id INT IDENTITY PRIMARY KEY, UserName NVARCHAR(50) NOT NULL, PasswordHash NVARCHAR(200) NOT NULL, Enabled BIT DEFAULT 1 ); CREATE TABLE Sys_Role ( Id INT PRIMARY KEY, RoleName NVARCHAR(50) NOT NULL ); CREATE TABLE Sys_UserRole ( UserId INT NOT NULL, RoleId INT NOT NULL ); CREATE TABLE Sys_Menu ( Id INT PRIMARY KEY, ParentId INT NULL, Name NVARCHAR(50) NOT NULL, Path NVARCHAR(200) NOT NULL, Component NVARCHAR(200) NOT NULL, PermissionCode NVARCHAR(100) NULL ); CREATE TABLE Sys_RoleMenu ( RoleId INT NOT NULL, MenuId INT NOT NULL );Sys_Menu 的 Component 字段对应 Vue 工程里的组件路径比如/stock/StockEdit.vuePermissionCode 是按钮级权限码比如erp:stock:edit。前端不决定谁能点哪个按钮后端登录成功后返回当前用户拥有的菜单树和权限码集合前端照渲染即可。权限要素存储位置维护方用途用户Sys_User管理员确认登录主体角色Sys_Role管理员组织权限集合菜单Sys_Menu开发者决定路由和按钮可见性绑定关系Sys_UserRole / Sys_RoleMenu管理员配置分配人员和功能范围这种设计把 ERP 最麻烦的权限分配交给配置后端管谁能进来、能看到哪些模块前端管这些模块怎么渲染。后面做成本核算、物料审核这类流程只需要在角色里加权限码。4.2 后端注册 JWT策略按权限码校验Program.cs 中注册 JWT 服务和授权策略这是 ASP.NET Core 6 之后的写法using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.Text; var builder WebApplication.CreateBuilder(args); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuer true, ValidIssuer builder.Configuration[Jwt:Issuer], ValidateAudience true, ValidAudience builder.Configuration[Jwt:Audience], ValidateIssuerSigningKey true, IssuerSigningKey new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration[Jwt:Secret])) }; }); builder.Services.AddAuthorization(options { options.AddPolicy(erp:stock:edit, policy policy.RequireClaim(permissions, erp:stock:edit)); }); var app builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();登录成功后生成 token 时要把权限码放进 Claim 集合注意 ClaimType 统一定为permissionsvar claims new ListClaim { new(ClaimTypes.Name, user.UserName), new(permissions, erp:stock:view), new(permissions, erp:stock:edit) };接口上直接标注策略[Authorize(Policy erp:stock:edit)] [HttpPost(api/stock/update)] public IActionResult UpdateStock(StockUpdateDto dto) { // 只有拥有 erp:stock:edit 权限码的用户能执行 }两个容易忽视的点第一同一 ClaimType 可以携带多个值策略里的 RequireClaim 第二个参数就是在匹配这个值集合第二只写[Authorize]不加 Policy只能校验登录状态控制不到按钮权限。ERP 里常见的“普通职员能打开采购单据但不能点审核”就用这种策略区分。4.3 前端用后端菜单表生成动态路由登录接口除了返回 token还返回当前用户可见的菜单树。前端拿到后做动态路由注册Vite 项目里用 import.meta.glob 映射组件const viewModules import.meta.glob(../views/**/*.vue) export function buildRoutesFromMenus(menus) { const routes [] for (const menu of menus) { const route { path: menu.path, component: viewModules[../views/${menu.component}.vue], meta: { title: menu.name, permission: menu.permissionCode }, children: [] } if (menu.children menu.children.length) { route.children buildRoutesFromMenus(menu.children) } routes.push(route) } return routes }这里容易踩一个坑component 路径必须与前端 views 目录里的文件大小写完全一致。Windows 上开发不敏感部署到 Linux 服务器上才会暴露典型的“vue 打包后布局异常”有一部分就是组件路径大小写写错导致的加载失败。父子级组件结构上父级菜单 Component 常指向 Layout 组件子级才指向具体页面。5. 正式环境部署Asp.net Core 发布与 Vue 静态文件托管5.1 dotnet publish 发布并配置 IIS 应用程序池后端编译发布命令很短cd Backend/ERP.Api dotnet publish -c Release -o ./publish发布目录里会生成可执行文件、一堆 dll 和 web.config。IIS 部署的关键不是复制文件而是两件事在服务器安装与 .NET 版本匹配的 ASP.NET Core Hosting Bundle然后新建站点时把应用程序池设为“无托管代码”。这两个顺序颠倒或漏掉站点打开就是 502.5 或 500.19 错误。配置项值站点物理路径publish 目录应用程序池No Managed Code绑定HTTP 或 HTTPS 端口默认文档web.config 会自动处理不需要配 index.htmlweb.config 里的 AspNetCoreModuleV2 是发布时自动生成的processPath 和 arguments 通常指向项目程序集一般不要手工改动。Hosting Bundle 安装完成后必须重启 IIS否则新站点会一直报进程无法启动。5.2 前端 build 后放到 wwwroot避免跨域前端打包同样简单cd Frontend npm run build生成 dist 目录。ERP 这种内网系统最常见的部署方式是“Vue 静态文件直接挂在 API 同域下”把 dist 里的文件拷到 publish 目录的 wwwroot再在 Program.cs 中启用静态文件支持app.UseStaticFiles(); app.MapFallbackToFile(index.html);UseStaticFiles 让浏览器请求/、/js/...时直接返回 wwwroot 下的静态文件MapFallbackToFile 是给 Vue Router history 模式兜底的关键用户刷新/stock/list时服务器没有这个物理路径会把请求回退到 index.html 交给前端路由处理。如果不加回退刷新一个子路由页面就会直接 404。生产环境里 Vue 打包后的资源路径也需要注意。如果站点部署在域名根目录vue.config.js 里保持 publicPath 为/即可如果放在子目录需要改成相对路径但 history 模式下相对路径刷新会有问题所以多数正式部署都选择根目录方案。5.3 环境变量与配置文件边界生产环境不建议直接改 appsettings.json而是新建 appsettings.Production.json{ ConnectionStrings: { Default: Serverprod-db;DatabaseERP_DB;User Iderp_app;Password高复杂度密码;TrustServerCertificateFalse }, Jwt: { Issuer: ERP.Api.Prod, Audience: ERP.Web, Secret: 重新生成的随机密钥 } }系统环境变量ASPNETCORE_ENVIRONMENTProduction是 .NET 读取对应配置的依据。部署时不要在服务器环境变量里塞开发库连接串否则它优先级高于配置文件会悄悄把生产环境带到开发库上去。前后端发布成同源单站点之后axios 的 baseURL 仍为/api和后端控制器路由前缀一致不需要额外开启 CORS。如果一定要把 API 和静态页面拆成两个站点后端 CORS 中间件要指定具体来源不要同时启用 AllowAnyOrigin 和 AllowCredentialsASP.NET Core 不允许这两个一起开。6. 把 Vue 动态路由接到后端权限表要改的三个细节6.1 Path 必须以 / 开头且父子不要重复拼接后端菜单表里的 Path 建议直接存/goods/list这种完整路径不要存goods/list。父级菜单存/goods子级菜单也存/goods/list前端 addRoute 时不再拼接避免 Vue Router 4 里空 path 父路由带来的重复匹配问题。调整历史数据时写一句更新即可UPDATE Sys_Menu SET Path /goods/list WHERE Id 21;6.2 addRoute 之后要显式 replace 一次目标地址动态路由注册是在登录后异步完成的此时当前导航还停留在登录前的路由表上直接 next() 会跳进 404。正确顺序是构建完动态路由后重新导航到当前地址const menus await getCurrentUserMenus() menus.forEach(m router.addRoute(toVueRoute(m))) next({ ...to, replace: true })replace 会替换当前历史记录用户按后退键也不会退回到登录后那个空白状态。6.3 按钮级权限码要保持唯一且贯穿一致sys_menu 表里的 permission_code 写成erp:stock:edit后端登录返回的 permissions 数组里也要有它前端指令再比对一次三处只要任何一处大小写或冒号不一致按钮就会无声消失。前端自定义指令一般这样写app.directive(permission, { mounted(el, binding) { const permissions useUserStore().permissions if (!permissions.includes(binding.value)) { el.remove() } } })页面里使用el-button v-permissionerp:stock:edit编辑/el-button像“审核”“过账”这类高危按钮如果只是对某部分人不可见不要直接删除节点可以改成 disabled 状态并用 Tooltip 提示原因更符合 ERP 系统的操作心智。后端返回的 permissions 数组要保持唯一不要把角色名混进去否则指令匹配时会出现意料之外的放行。本文还有配套的精品资源点击获取
返回列表