C# .NET 8 + Vue 3 前后端分离仓库管理系统设计与实现

发布时间:2026/8/31 2:40:58
C# .NET 8 + Vue 3 前后端分离仓库管理系统设计与实现 简介这是一套基于C# .NET Web API与Vue.js实现的前后端分离式仓库管理系统完整源码面向需要企业级项目实战的开发者、毕业设计学生及求职者解决仓储业务中商品管理、库存跟踪、用户权限控制等核心场景开发难题。资源包含243个文件涵盖112个C#后端逻辑文件含Web API控制器、JWT鉴权过滤器、Swagger文档配置、19个Vue前端组件、23个CSHTML视图页、20个CSS样式文件及6个关键配置文件如Web.config、.csproj整体压缩包仅9.48MB轻量易部署。已有767人学习下载代码结构清晰已集成跨域处理含Options预检方法、Bearer JWT认证含Token生成、登录返回昵称token、全局校验过滤器及Swagger接口文档自动生成便于快速理解前后端协作流程与安全机制实现细节。 做仓库管理系统最怕什么不是写功能而是写完上线之后发现库存对不上、单据流转卡壳、权限混乱、数据一多就卡。这套基于 C# .Net 和 Vue 实现的前后端分离仓库管理系统从后端 API 到 PC 端管理界面都覆盖了入库、出库、盘点、库存预警、角色权限这些核心模块全部走通适合正在做进销存、仓储信息化项目或者想学习前后端分离实战的开发者参考。我最初选型的时候也纠结过要不要用 Spring Boot但考虑到团队里 C# 存量代码多、Windows 服务器部署方便最后定了 .NET 8 Vue 3 Element Plus 这套组合。项目跑起来之后后端负责业务逻辑和数据前端只做界面交互两边通过 JSON 通信整体结构清晰后续接手持终端扫码枪也比较容易。这篇博文把整个项目的设计思路、实现细节、部署过程和踩坑记录一次性说清楚。1. 项目概览与技术选型思路1.1 仓库管理系统的核心业务需求先抛开技术谈需求。任何一个仓库管理系统不管外表多花哨最终要解决的就三件事账实相符、进出有据、库存可追溯。围绕着三件事系统必拆的模块其实很固定基础资料、入库作业、出库作业、库存查询、盘点、预警、报表外加一个贯穿全局的权限管理。基础资料管的是商品、仓库、库位、供应商、客户这些元数据。入库出库是每天最高频的操作PC 端操作员的体验直接决定他们愿不愿意用。库存查询要快操作员查一个 SKU 的库存不该等三秒。盘点是个脏活但是不做账迟早烂掉系统里要有盘点任务生成、差异调整这一套。预警则是给管理层用的库存低于安全值或者临期要能自动提醒。这些需求确定之后再去定技术方案才不会跑偏。很多人一上来就纠结数据库用什么、ORM 用什么结果业务没理清做了个四不像。我的建议是先把模块和数据关系画出来再谈技术。1.2 为什么选择 C# .Net 而不是 Java 全家桶身边不少朋友问我为什么做前后端分离不直接用 Spring Boot这里没有谁好谁坏而是看场景。做仓库管理系统这类偏企业内部的业务系统C# .Net 有几个很实在的优势。第一开发效率。EF Core 在 CRUD 场景下写代码是真的快尤其是配合导航属性和 LINQ比写一大堆 MyBatis XML 要省事。第二Windows 部署生态成熟很多传统企业的服务器就是 WindowsIIS 上发布一个 .NET 应用比装 Tomcat 更顺手。第三.NET 8 之后性能提升明显底层库甚至比 Node.js 更适合处理大批量数据操作。当然 Java 生态的 Spring Boot 也不是不行只是多一个选择和权衡的问题。我这套系统里也对比过如果未来要大规模上微服务、Kafka 那套Java 的社区资源更丰富但中小型仓储系统单体应用就够了没必要为了技术而技术。有人会提学 Spring Boot Vue那是另一条路并不影响这里的选型逻辑。1.3 整体架构与目录结构这套系统的架构图其实特别简单没有微服务没有消息队列就是经典的前后端分离单体应用。WmsSystem/ ├── server/ # 后端服务 │ ├── Wms.Api/ # Web API 层控制器、过滤器、中间件 │ ├── Wms.Application/ # 应用服务层业务逻辑 │ ├── Wms.Domain/ # 领域层实体、枚举、接口 │ ├── Wms.Infrastructure/ # 基础设施层EF Core、仓储实现 │ └── Wms.sln └── web/ # 前端 PC 端 ├── src/ │ ├── api/ # 接口请求封装 │ ├── assets/ # 静态资源 │ ├── components/ # 公共组件 │ ├── router/ # 路由配置 │ ├── stores/ # Pinia 状态管理 │ ├── views/ # 页面视图 │ ├── utils/ # 工具函数 │ └── main.js ├── package.json └── vite.config.js后端分了四层Api 层只做参数接收和结果返回Application 层处理业务规则Domain 层放实体和核心接口Infrastructure 层管数据库访问。这种分层的好处是职责单一比如以后想从 EF Core 换 Dapper只需要改 Infrastructure不影响上面的业务逻辑。前端就是标准的 Vue 工程页面、组件、路由、状态管理各归其位。Api 目录专门做接口封装页面里不允许直接写 axios避免请求散落到处都是。1.4 功能模块全景图模块功能说明核心数据表登录认证JWT 签发、刷新、角色识别SysUser, SysRole基础资料商品档案、仓库、库位、往来单位Product, Warehouse, Supplier入库管理采购入库、退货入库、入库审核InboundOrder, InboundOrderItem出库管理销售出库、领料出库、出库审核OutboundOrder, OutboundOrderItem库存管理实时库存、库存流水、库存锁定Stock, StockLog盘点管理盘点任务生成、盘点录入、差异调整InventoryTask, InventoryTaskItem预警中心库存上下限预警、保质期预警StockWarning, Product报表统计出入库日报、库存周转、月度汇总视图或聚合查询每个模块之间不是孤立的入库单审核通过会写库存和流水出库单审核通过会扣库存并预留数量盘点调整会修正库存并留痕。这套业务链路在设计数据库时就得想清楚。2. 后端 Server 端核心实现2.1 框架版本与项目结构构建我用的是 .NET 8这是目前 LTS 版本里最适合新项目的选择。如果一个团队还在纠结 .NET Framework 3.5那只能说明系统里有老代码要兼容。新项目直接用 .NET 8跨平台、性能好、依赖注入和配置系统都是内置的少装一堆第三方包。创建项目的命令很简单dotnet new sln -n Wms dotnet new webapi -n Wms.Api -o server/Wms.Api dotnet new classlib -n Wms.Application -o server/Wms.Application dotnet new classlib -n Wms.Domain -o server/Wms.Domain dotnet new classlib -n Wms.Infrastructure -o server/Wms.Infrastructure dotnet sln add server/Wms.Api server/Wms.Application server/Wms.Domain server/Wms.Infrastructure然后在 Api 项目里引用 ApplicationApplication 引用 DomainInfrastructure 引用 Domain 和 Application引用方向是单向的不允许 Domain 反向引用其他层。服务注册我放在 Api 项目的 Program.cs 里写成扩展方法方便管理builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddDbContextWmsDbContext(opt opt.UseSqlServer(builder.Configuration.GetConnectionString(Default))); builder.Services.AddApplicationServices(); builder.Services.AddInfrastructureServices();2.2 数据库表设计与 EF Core 映射仓库管理系统最核心的表设计有六张第一张是商品表第二张是仓库表第三张是库存表第四张是入库表第五张是出库表第六张是盘点表。围绕这六张再挂子表和流水表。商品表里面有一个容易忽略的点我加了拼音码字段在后端用 C# 写了个取汉字拼音首字母的扩展方法操作员录入时输入 XJ 就能搜到 笔记本电脑 这类商品这个体验提升非常明显。public class Product { public long Id { get; set; } public string SkuCode { get; set; } // 商品编码 public string Name { get; set; } // 商品名称 public string? PinyinCode { get; set; } // 拼音助记码 public string? Specification { get; set; } // 规格型号 public string? Unit { get; set; } // 单位 public int SafetyStock { get; set; } // 安全库存 public int CurrentStock { get; set; } // 当前库存冗余字段用于列表快速展示 }库存表 Stock 是并发控制的重灾区我在里面加了 RowVersion 字段。EF Core 里配置方法如下public class Stock { public long Id { get; set; } public long ProductId { get; set; } public long WarehouseId { get; set; } public int StockQty { get; set; } public byte[] RowVersion { get; set; } } // Fluent API 配置 modelBuilder.EntityStock() .Property(s s.RowVersion) .IsRowVersion();查询库存操作放在一个事务里先根据商品 ID 和仓库 ID 查出库存记录判断数量是否足够再扣减如果期间有人改了这条记录数据库会抛 DbUpdateConcurrencyException我们再决定重试还是回滚。这套逻辑虽然简单但是很实用。2.3 统一响应模型与全局异常处理前后端分离之后接口返回值必须有一套约定。如果每个接口都返回不同的结构前端封装 axios 的时候就很难统一处理。我定义了一个 ApiResponse 统一包装所有响应。public class ApiResponseT { public int Code { get; set; } public string Message { get; set; } public T Data { get; set; } public static ApiResponseT Success(T data, string message ok) new() { Code 0, Message message, Data data }; public static ApiResponseT Fail(string message, int code 500) new() { Code code, Message message }; }Controller 里所有正常返回都走 Success业务异常直接抛一个 BusinessException在中间件里统一捕获。这样前端拿到 Code 不是 0 的时候就知道业务出错了不用每个接口重复写 try-catch。异常处理中间件也很简单捕获 BusinessException 返回 200 加业务错误码捕获其他异常记录日志后返回 500。注意不要返回堆栈信息给前端内部错误细节写日志就够了。2.4 JWT 身份认证与角色权限控制仓库系统不同角色能看的页面和能做的操作必须分开普通仓管员不能随便删单据管理员才能配置权限。我用 JWT 做认证登录成功签发带角色信息的 Token每个请求带着 Token 到后端从 Claims 里取角色。配置 JWT 服务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], ValidateLifetime true, IssuerSigningKey new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration[Jwt:Key])), ClockSkew TimeSpan.FromMinutes(1) }; });登录接口里生成 Token 时把用户 ID、用户名、角色 ID 都塞进 Claims。后端控制器加 [Authorize(Roles Admin)] 就能控制接口权限。前端再根据角色动态生成菜单双保险。这里有个坑Token 有效期设太短用户频繁重登设太长泄露风险加大。仓储系统内部用我设了 12 小时另加一个 RefreshToken 表做续期公司内部用起来体验还行。2.5 库存流转核心逻辑入库、出库、盘点这一块是系统的核心也是最容易出并发问题的地方。以入库单审核为例完整流程是启动事务、逐条明细判断商品和仓库是否存在、更新库存数字、写库存流水、更新单据状态、提交事务。核心代码大致是这样public async Tasklong ConfirmInboundAsync(long orderId) { using var transaction await _db.Database.BeginTransactionAsync(); try { var order await _db.InboundOrders .Include(o o.Items) .FirstOrDefaultAsync(o o.Id orderId); if (order null || order.Status ! OrderStatus.Pending) throw new BusinessException(单据不存在或状态不允许审核); foreach (var item in order.Items) { var stock await _db.Stocks .FirstOrDefaultAsync(s s.ProductId item.ProductId s.WarehouseId item.WarehouseId); if (stock null) { stock new Stock { ProductId item.ProductId, WarehouseId item.WarehouseId }; _db.Stocks.Add(stock); } stock.StockQty item.InboundQty; _db.StockLogs.Add(new StockLog { ProductId item.ProductId, WarehouseId item.WarehouseId, ChangeQty item.InboundQty, BeforeQty stock.StockQty - item.InboundQty, AfterQty stock.StockQty, LogType 入库, RelatedOrderNo order.OrderNo }); } order.Status OrderStatus.Confirmed; await _db.SaveChangesAsync(); await transaction.CommitAsync(); return order.Id; } catch { await transaction.RollbackAsync(); throw; } }出库逻辑类似只是扣减前要判断库存是否充足。盘点则是先生成盘点单盘点完成后在事务里把差异数修正到库存里同时保留差异调整流水方便财务对账。2.6 定时任务与库存预警实现库存预警要做成定时任务每天凌晨扫描一次商品表把低于安全库存的商品写入预警表也可以通过邮件或企业微信通知到相关负责人。.NET 8 里我没引入 Quartz直接用 BackgroundService 做后台任务就够了public class StockWarningWorker : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; public StockWarningWorker(IServiceScopeFactory scopeFactory) _scopeFactory scopeFactory; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using var timer new PeriodicTimer(TimeSpan.FromHours(6)); while (await timer.WaitForNextTickAsync(stoppingToken)) { using var scope _scopeFactory.CreateScope(); var service scope.ServiceProvider.GetRequiredServiceIStockWarningService(); await service.CheckStockWarningsAsync(); } } }注意 BackgroundService 里解析 DbContext 不能直接用构造函数注入的实例因为它是单例而 DbContext 是 Scope 生命周期直接注入会出问题。正确做法是通过 IServiceScopeFactory 创建新 Scope。3. 前端 PC 端实现3.1 Vue 项目环境搭建与工程化配置前端这块我选了 Vue 3 加 Vite 的组合。Vite 开发时的热更新速度比 Webpack 快太多项目大了也很稳装依赖后跑npm run dev基本秒开。用 Vue CLI 的老项目可以慢慢迁移新项目直接 Vite。初始化命令一行搞定npm create vitelatest web -- --template vue cd web npm install npm install element-plus axios pinia vue-router echartsVite 配置文件里关键的是开发环境代理后端 API 地址是 localhost:5000前端项目是 localhost:5173不配代理就会遇到 Failed to load resource net::ERR_CONNECTION_REFUSED 这类问题// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } } });3.2 路由守卫、登录态管理与 axios 封装路由守卫是这个项目前端最值得说的地方。仓库管理系统的页面不是所有人都有权限看所以我在路由 meta 里加了 roles 字段登录后从接口拉取用户权限动态生成路由。Vue Router 全局前置守卫大概长这样router.beforeEach((to, from, next) { const token localStorage.getItem(wms_token); if (!token) { if (to.path /login) next(); else next(/login); return; } const userStore useUserStore(); if (!userStore.permissions.length) { userStore.fetchUserInfo().then(() { if (to.meta.roles !to.meta.roles.includes(userStore.role)) { next(/403); } else { next(); } }); return; } next(); });axios 封装要处理的是三件套请求带 Token、统一错误提示、401 跳登录。用拦截器实现。http.interceptors.request.use(config { const token localStorage.getItem(wms_token); if (token) config.headers.Authorization Bearer ${token}; return config; }); http.interceptors.response.use( res { const { code, message, data } res.data; if (code 0) return data; ElMessage.error(message || 业务出错); return Promise.reject(new Error(message)); }, err { if (err.response?.status 401) { router.push(/login); } else { ElMessage.error(err.message || 网络异常); } return Promise.reject(err); } );3.3 核心页面实现入库单的完整交互以入库单为例这个页面包含了仓库管理系统最常见的交互模式列表查询、新增编辑、详情审核。列表区用 Element Plus 的 el-table分页器做在页面底部搜索条件做成折叠面板。新增入库单的弹窗里商品选择是关键交互。操作员要能通过商品编码、名称、拼音码快速搜到商品选中后回填规格、单位然后输入数量。我用 el-select 加 remote 远程搜索每次输入触发接口查询配合防抖函数避免频繁请求。Vue 代码片段如下el-select v-modelform.productId filterable remote :remote-methodsearchProduct placeholder输入编码/名称/拼音码搜索 el-option v-forp in productOptions :keyp.id :label${p.skuCode} ${p.name} :valuep.id / /el-select选完商品之后明细表格里要显示商品信息数量默认填 1操作员改数量时校验不能超过某个合理值比如单次入库上限。审核提交流程走后端事务前端只需要 disable 掉按钮防止重复提交。3.4 表格加载优化与权限指令仓库数据量上来以后表格渲染会卡。我做了三件事第一是列表接口必须分页第二是表格列不要一次性塞 20 列第三是远程搜索加防抖。防抖函数很简单300 毫秒内只触发最后一次请求function debounce(fn, delay 300) { let timer null; return function(...args) { clearTimeout(timer); timer setTimeout(() fn.apply(this, args), delay); }; } const searchProduct debounce(async (keyword) { productOptions.value await productApi.search(keyword); });权限控制除了路由守卫还做了按钮级控制。自定义指令 v-permission 判断当前用户角色是否有指定权限码没有就移除 DOM避免用户看到按钮点了却提示无权限的尴尬。3.5 数据可视化与地图定位扩展仓库管理系统给管理层看的页面不能纯表格要有图表。成品里我用了 ECharts 做库存分析和出入库趋势Vue 3 下有 vue-echarts 封装图表更新只需改 option很方便。另外如果公司有多个仓可以用腾讯地图在 Web 端展示仓库分布每个仓库用 Marker 标注当前位置点击 Marker 弹窗显示该仓库的今日出库量、库存金额等核心指标。这个功能官网文档有现成接入示例注意申请 Key 之后配置域名白名单不然本地调试会出现地图加载失败。4. 前后端联调与部署上线4.1 开发环境的跨域与代理配置前后端分离项目开发阶段最绕不开的问题是跨域。前端在 5173 端口后端在 5000 端口浏览器默认禁止跨域请求。解决方案有两个后端开启 CORS或者前端 Vite 配代理。生产环境一般用代理开发环境我也推荐代理因为可以隐藏后端实际地址部署的时候不用改前端代码。Vite 代理配置上面已经写了后端 CORS 作为兜底也可以配一下防止以后有人直接用 Swagger 调试被浏览器拦截builder.Services.AddCors(opt { opt.AddPolicy(AllowAll, policy policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()); });联调的时候如果遇到 Network connection timeout先检查代理 target 地址对不对再看后端端口是否真的被监听最后确认防火墙是否放行。百分之八十的联调问题都出在这三处。4.2 生产构建与静态文件托管前端打包前要把接口请求地址改成正式环境地址。我习惯用 Vite 的环境变量# .env.production VITE_API_BASE_URL/api打包命令npm run build会生成 dist 目录后端可以直接托管这些静态文件也可以单独部署到 Nginx。如果托管在后端需要在 Program.cs 里加一句app.UseStaticFiles(); app.MapFallbackToFile(index.html);这个 MapFallbackToFile 很重要Vue Router 用了 history 模式后直接访问 /dashboard 刷新会 404加上这段就能让所有未知路由回到 index.html再由前端路由接管。如果忘了配生产环境刷新页面就一片白排查半天才反应过来是路由兜底的问题。4.3 服务端部署方式选择这套系统我试过两种部署方式。第一种是 IIS直接发布 server 项目到目录创建站点指向发布目录应用程序池选择无托管代码因为 Kestrel 自己就是服务器。第二种是 Windows 服务用 sc 命令或者 NSSM 把 dotnet 命令包装成服务适合内网服务器没有 IIS 的环境。Windows 服务部署简单记一下先用 dotnet publish 发布dotnet publish -c Release -o publish然后用 NSSM 将 exe 注册为服务NSSM 填写程序路径为 dotnet.exe参数为 Wms.Api.dll工作目录指向 publish 即可。4.4 部署环境常见报错排查部署到新机器上最容易碰到 .NET 运行时缺失的问题。.NET 8 应用在没装 .NET Desktop Runtime 的机器上启动会直接弹窗提示 You must install .NET Desktop Runtime这时候下载对应版本的运行时装上就好。注意区分 ASP.NET Core Runtime 和 Desktop RuntimeWeb 应用需要前者桌面程序需要后者。老机器上如果提示需要 .NET Framework 3.5这通常是应用配置里指定了旧版目标框架。新版 .NET 8 项目不需要这个组件确认 csproj 里的 TargetFramework 是 net8.0 就行。还有一次遇到 Oracle 数据库连接报 ORA-28547后来发现是连接串里的 Service Name 配错了跟后端代码没关系。数据库连接串出问题先看网络能不能通再用数据库客户端工具测一次连接往往能快速定位。5. 常见问题与排查技巧实录5.1 后端启动报错无法加载一个或多个请求类型这个报错比较典型原文是 C# 无法加载一个或多个请求的类型。有关更多信息请检索 LoaderExceptions 属性。 本质是程序集版本冲突或者某个依赖项没拷贝到输出目录。排查步骤三步走。第一步把项目引用的 NuGet 包版本统一尤其是 EF Core、AutoMapper 这类容易带传递依赖的包。第二步检查 bin 目录看是否缺少对应 DLL如果发布后部署还报错用 dotnet 的dotnet publish重新发布。第三步写一段临时代码捕获 LoaderExceptions 并输出详细信息try { AppDomain.CurrentDomain.Load(Wms.Application); } catch (Exception ex) { var loaderEx ex as System.Reflection.ReflectionTypeLoadException; if (loaderEx ! null) { foreach (var inner in loaderEx.LoaderExceptions) { Console.WriteLine(inner?.Message); } } }这个脚本能打印出到底哪个程序集加载失败是版本不匹配还是强签名问题一看便知。5.2 前端路由刷新 404 与 DevTools 检测不到 Vue前端两个高频问题。第一个是 history 模式刷新 404前面说了后端加 MapFallbackToFile 可以解决如果用了 Nginx要配置 try_files 参数location / { try_files $uri $uri/ /index.html; }第二个是 Vue DevTools 插件检测不到应用常见原因是项目通过 Vite 插件挂了多个 Vue 实例或者 DevTools 版本跟 Vue 3 不匹配。新版 DevTools 直接支持 Vue 3装了之后如果还是显示未检测到检查 extension 是否授予了当前站点访问权限。5.3 库存并发超卖与负数问题高并发下出库操作如果没控制好很容易出现库存变负数。我压测的时候遇到过这类情况两个窗口同时对一个 SKU 出库各自都查到库存 10各出 8结果库存变成 -6。解决方案是前面提到的 RowVersion 并发控制更新时带上原来的版本号如果 Update 影响行数为 0 说明已经被别人改过重新查询并提示操作员重试。更激进一点的办法是在数据库层做原子扣减UPDATE Stock SET StockQty StockQty - qty WHERE ProductId pid AND WarehouseId wid AND StockQty qty这样即使并发量大数据库行锁也能保证不会超扣但要注意把 UPDATE 放在事务里并检查影响行数。5.4 排查工具与调试经验汇总问题现象可能原因处理建议接口返回 500后端异常未处理查看日志用 Swagger 或 Postman 复现前端一直转圈请求被 CORS 拦截浏览器 F12 看 Network确认是否跨域数据库连接超时连接串写错或网络隔离用 SSMS 客户端测试连接定时任务不触发BackgroundService 未注册检查 AddHostedService 是否注册Token 过期但未跳登录前端 401 拦截器没生效检查 axios 响应拦截器顺序上传文件失败请求头没带 Content-Type用 FormData 提交不要手动设置调试小提示前端尽量多用 Vue DevTools 的组件面板看状态而不是在控制台 console.log 一堆。后端多用日志Serilog 写文件或者接一个日志平台都行不要只在开发环境断点调试。写在最后的实操心得这套系统从搭建到跑通我个人的体会是前后端分离本身不难难的是把边界划分清楚。后端只管数据和业务规则前端只管交互和展示两边约定好接口格式就按部就班推进。中间踩过最深的坑不是技术而是没提前定好统一响应模型导致每个接口返回结构都不一样前后端联调时改数据格式改到怀疑人生。后来把所有接口统一成 ApiResponse 前端封装一层拦截器整个联调效率直接翻倍。还有一个小技巧开发期间把后端的 Swagger 打开前端遇到接口问题先自己在 Swagger 里调一遍能分清是后端代码问题还是前端传参问题别一上来就互相甩锅。这套系统后续要扩展也不难加一个移动端 H5 或者小程序直接复用 server 端接口前端另起一个项目就行。你现在照着这套思路做至少能少走一半弯路。本文还有配套的精品资源点击获取