简介:这是一套基于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 部署生态成熟,很多传统企业的服务器就是 Windows,IIS 上发布一个 .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 项目里引用 Application,Application 引用 Domain,Infrastructure 引用 Domain 和 Application,引用方向是单向的,不允许 Domain 反向引用其他层。
服务注册我放在 Api 项目的 Program.cs 里,写成扩展方法方便管理:
builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddDbContext<WmsDbContext>(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.Entity<Stock>() .Property(s => s.RowVersion) .IsRowVersion();查询库存操作放在一个事务里,先根据商品 ID 和仓库 ID 查出库存记录,判断数量是否足够再扣减,如果期间有人改了这条记录,数据库会抛 DbUpdateConcurrencyException,我们再决定重试还是回滚。这套逻辑虽然简单,但是很实用。
2.3 统一响应模型与全局异常处理
前后端分离之后,接口返回值必须有一套约定。如果每个接口都返回不同的结构,前端封装 axios 的时候就很难统一处理。我定义了一个 ApiResponse 统一包装所有响应。
public class ApiResponse<T> { public int Code { get; set; } public string Message { get; set; } public T Data { get; set; } public static ApiResponse<T> Success(T data, string message = "ok") => new() { Code = 0, Message = message, Data = data }; public static ApiResponse<T> 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 Task<long> 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.GetRequiredService<IStockWarningService>(); 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 vite@latest 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-model="form.productId" filterable remote :remote-method="searchProduct" placeholder="输入编码/名称/拼音码搜索"> <el-option v-for="p in productOptions" :key="p.id" :label="`${p.skuCode} ${p.name}`" :value="p.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 Runtime,Web 应用需要前者,桌面程序需要后者。
老机器上如果提示需要 .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 端接口,前端另起一个项目就行。你现在照着这套思路做,至少能少走一半弯路。
本文还有配套的精品资源,点击获取