简介:面向ASP.NET MVC开发者与仓库管理系统项目学习者,这份源码是一套完整可运行的仓库管理解决方案,涵盖入库、出库、库存查询、基础资料维护等典型业务模块,适用于课程设计、毕业设计或实际项目二次开发。压缩包共42.7MB,包含2821个文件,其中455个JavaScript文件负责前端交互,400个C#源文件承载业务逻辑,450个PNG图片与164个CSS样式完善界面表现,102个CSHTML视图则对应MVC的视图层,另有DLL、HTML、XML等辅助文件覆盖常见Web项目结构。系统使用Visual Studio 2019或2022即可打开,数据库支持MySQL与SQL Server,修改连接字符串即可快速部署。代码分层清晰,数据访问、业务处理与页面展示相互独立,既适合初学者理解MVC开发流程,也便于有经验的开发者在此基础上改造或扩展;从实体建模、数据访问到页面呈现,整体脉络完整清晰,是学习业务系统组织方式的良好范例。目前已有206人学习浏览,对于需要搭建仓库管理系统的技术人群是一份值得参考的完整源代码。 前段时间刚好帮一个做电商的朋友把仓库管理从Excel搬到了系统上,选型的时候对比了PHP、Java,最后用了ASP.NET MVC。这套东西写完之后一直放在Git里当模板用,有几次社区里有人问能不能分享一下完整源码,我干脆把整个设计思路、核心代码和踩过的坑整理成一篇,一方面给正在选型的团队一个参考,另一方面也让刚接触MVC的朋友有个能照着做的完整例子。
这篇不是那种"教你写HelloWorld"的入门文,而是从真实项目出发,把一个仓库管理系统从业务分析到表结构设计、从EF数据操作到IIS部署的完整链路走一遍。如果你正在做进销存、仓储、ERP类的管理系统,或者想找一个ASP.NET MVC实战项目来练手,这篇的内容可以直接拿来改。
1. 项目整体设计与架构思路
1.1 仓库管理到底在管什么
很多人一听"仓库管理系统"就觉得是个进销存,其实真做起来业务比想象中要复杂。仓库管理系统的核心不是"录入数据",而是保证实物库存、账面库存、业务单据三者始终保持一致,并且能够追踪任意一笔库存变动的来源。
以我这里这套系统为例,核心业务模块围绕五个方向展开:
- 商品资料管理:维护商品编码、名称、规格、单位、分类、条码、默认供应商等信息。商品编码是整个系统的关键索引,设计时我用了"编码即规则"的思路,通过编码前缀就可以分辨商品分类(比如"SP-LJ-001"就是"商品-零配件-序号"),这比单独建分类字段更直观,但也对编码规则提出了很高的要求,必须在录入时就做唯一性校验。
- 入库管理:包含采购入库、退货入库、盘盈入库三种类型。入库单需要记录供应商、入库仓库、经办人、入库明细(商品、数量、单价、生产日期、有效期)。
- 出库管理:包含销售出库、领料出库、盘亏出库三种类型。出库单必须校验库存是否充足,不够时直接拦截并提示。
- 库存查询:按仓库、商品、批次查看当前库存数量和可用库存,支持多条件组合查询。
- 报表统计:入库汇总、出库汇总、库存周转、各仓库库存金额等基础报表。
注意:业务边界一定要在开发前理清楚,否则数据库表设计出来以后想改,代价特别大。我第一个版本就因为没有区分"退货入库"和"采购入库",导致后面加了一个标记字段,还被迫重写了报表统计逻辑。
1.2 技术选型:ASP.NET MVC相比其他方案强在哪
技术选型的时候我其实纠结了一段时间。PHP上手快,Java招聘多,为什么最后选了ASP.NET MVC?
一个核心原因是类型安全和开发效率的平衡。ASP.NET MVC使用强类型视图和模型绑定,编译期就能发现大部分类型错误,配合Visual Studio的调试体验,开发周期明显更短。团队里如果有C#基础的同学,上手周期大约一到两周就能独立写功能模块。
第二个原因是MVC模式天然的职责分离。Model负责业务和数据,View只做展示,Controller负责接收请求和调度。仓库管理系统最常见的修改需求是"列表页加一列""增加一个筛选条件",MVC的模式下改一个ViewModel和对应的View就行,不会动到业务逻辑层,这点在长期维护时特别重要。
第三个原因,与微软技术栈的整合成本低。如果需要对接线上商城的API、对接Excel导出、做报表服务,.NET平台都有现成的库和组件,部署在Windows Server + IIS环境上也十分简单。
当然,如果是纯互联网高并发场景,ASP.NET Core的性能表现更均衡。但仓库管理系统这种企业内部系统,用户量通常几十到几百人,并发量不大,.NET Framework 4.7.2 + MVC 5这套组合已经是成本最低、稳定性极高的方案。
1.3 MVC三层架构:目录结构决定了代码的宿命
项目结构直接决定了后续维护的舒适度。这套系统的解决方案里,我分成了三个项目(Project),对应经典的三层架构:
仓库管理系统.sln ├── WM.Web(表现层,MVC项目) │ ├── Controllers(控制器) │ ├── Views(视图) │ ├── Models(ViewModel) │ └── Content/Scripts(静态资源) ├── WM.BLL(业务逻辑层) │ ├── ProductManager.cs │ ├── StockManager.cs │ ├── InboundManager.cs │ └── OutboundManager.cs └── WM.DAL(数据访问层) ├── EFDbContext.cs(数据上下文) ├── Repository<T>.cs(通用仓储) └── Migrations(数据库迁移)三层架构最容易犯的错误是"层与层之间乱引用"。我的原则是:UI只能引用BLL,BLL只能引用DAL,DAL不引用任何上层。如果Controller里出现了对DbContext的直接操作,那说明层已经破了。
实操心得:不要为了"节省代码量"跳过业务层,直接在Controller里写数据库操作。刚开始觉得快,后期要加权限校验、库存日志、通知推送这些逻辑时,你会发现自己要在十几个Controller里重复改同一段代码,而放在BLL里只需要改一处。
2. 数据库设计与核心功能实现
2.1 五张基础表的表结构设计
数据库我用的SQL Server 2016,选它的原因很简单:和Entity Framework(后面简称EF)的兼容性最好,而且SQL Server的事务机制和行版本控制比较成熟,适合库存这类对数据一致性要求高的场景。
核心表设计如下(仅列关键字段):
Product(商品表)
| 字段名 | 类型 | 说明 |
|---|---|---|
| Id | int(主键,自增) | 记录唯一标识 |
| ProductCode | nvarchar(50) | 商品编码,唯一索引 |
| ProductName | nvarchar(200) | 商品名称 |
| CategoryId | int | 分类外键 |
| Unit | nvarchar(20) | 计量单位 |
| Price | decimal(18,2) | 参考进价 |
| IsDeleted | bit | 逻辑删除标记 |
InboundOrder / InboundOrderDetail(入库单/入库明细)
主表存储单据编号、供应商、仓库、入库类型、经办人、入库时间、备注;明细表存储商品Id、入库数量、单价、金额、生产日期、有效期。主表和明细表通过InboundOrderId外键关联。
OutboundOrder / OutboundOrderDetail(出库单/出库明细)
结构与入库表类似,增加了"出库原因"字段。出库明细表里有一个"库存记录Id",用来关联实际扣减的库存批次。
Stock(库存表)
| 字段名 | 类型 | 说明 |
|---|---|---|
| Id | int(主键) | 记录唯一标识 |
| ProductId | int | 商品外键 |
| WarehouseId | int | 仓库外键 |
| Quantity | int | 当前库存数量 |
| LockedQuantity | int | 锁定数量(订单占用) |
| BatchNo | nvarchar(50) | 批次号 |
重要设计决策:库存表不只在产品维度上做唯一约束,而是在"商品+仓库+批次"三个维度上做唯一索引。起因是实际业务里有保质期管理需求——同一款饮料,不同批次的进货价和保质期是不同的,出库时要优先出库先到期的批次(这就是FIFO,先进先出)。如果库存表只按商品粒度存储一个总数,FIFO完全无法实现。
2.2 入库和出库的库存联动逻辑
这块是整个系统最核心的业务逻辑,也是我踩坑最多的地方。入库和出库不是简单的"库存加数量""库存减数量",而是一个事务性操作。
入库逻辑(简化版):
1. 创建InboundOrder主表记录 2. 遍历入库明细,逐条创建InboundOrderDetail 3. 检查Stock表中是否已存在"商品+仓库+批次"对应的库存记录 存在 => 库存数量累加 不存在 => 新建一条Stock记录 4. 写入StockLog(库存变动日志) 5. 如果步骤2-4任一步失败,整个事务回滚出库逻辑(简化版):
1. 校验出库明细中每种商品的总库存是否充足 2. 按FIFO原则,先扣减最早批次的库存 3. 扣减前检查LockedQuantity(防止超卖) 4. 写入StockLog(库存变动日志) 5. 事务回滚条件下保证账面一致性第4步的StockLog是我后来加上的,也正是这个表救了我一次。当时上线后运营反馈某商品库存对不上,如果没有操作日志,这个问题可能得排查好几天。有了StockLog,直接按时间倒序查询该商品所有变动记录,十分钟就定位到是某个入库单在编辑时重复提交导致的。
EF里开启事务的做法很简单:
using (var transaction = db.Database.BeginTransaction()) { try { // 1. 保存入库单主表 db.InboundOrders.Add(inboundOrder); await db.SaveChangesAsync(); // 2. 逐条处理库存 foreach (var item in details) { await UpdateStockAsync(item); } // 3. 记录日志 db.StockLogs.Add(log); await db.SaveChangesAsync(); // 4. 提交 transaction.Commit(); } catch { transaction.Rollback(); throw; } }2.3 权限控制与安全设计:Session还是Token
仓库管理系统的用户角色可以简单分成三种:管理员、仓管员、财务/审计(只读)。权限控制我用了自定义AuthorizeAttribute + Session的组合方案。
MVC自带的[Authorize]特性只能判断"是否登录",不能细分角色。我继承了这个特性做了一层扩展:
public class WarehouseAuthorizeAttribute : AuthorizeAttribute { public string Role { get; set; } protected override bool AuthorizeCore(HttpContextBase httpContext) { var user = httpContext.Session["CurrentUser"] as UserModel; if (user == null) return false; if (string.IsNullOrEmpty(Role)) return true; return user.Role == Role; } protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { if (filterContext.HttpContext.Session["CurrentUser"] == null) { filterContext.Result = new RedirectResult("/Account/Login"); } else { filterContext.Result = new ViewResult { ViewName = "NoPermission" }; } } }用法就是在Controller或Action上标注:
[WarehouseAuthorize(Role = "Admin")] public ActionResult Index() { return View(); }关于安全,有一点要特别提醒:永远不要信任前端传过来的任何数据。仓库管理系统的表单很多,我用的是ViewModel + 数据注解校验,而不是直接绑定EF实体。直接绑定实体有安全隐患——如果数据库里有个IsAdmin字段,攻击者可以在表单里伪造这个字段名,模型绑定器就会自动给它赋值。
注意:从MVC 4开始,默认情况下如果视图输出中包含
<script>等标签,ValidateRequest会主动拦截,报"检测到有潜在危险的 Request.Form 值"错误。这个机制是为了防XSS攻击。不过系统的商品名称等字段确实可能需要录入特殊字符(比如"<5mm"这种规格),解决办法是先确认业务确实需要,然后在对应ViewModel上标注[AllowHtml],而不是全局关闭校验。全局关闭的风险远大于收益,千万别这么干。
3. 实操过程:从空项目到核心功能跑通
3.1 开发环境准备与项目脚手架
我的开发环境是Visual Studio 2019 + .NET Framework 4.7.2 + SQL Server 2016。创建项目的步骤比较简单,直接选"ASP.NET Web应用程序(.NET Framework)",然后在模板里选"MVC",注意框架版本要选4.7.2,身份验证选"不进行身份验证"(后面自己实现权限控制,避免模板代码干扰)。
NuGet包需要安装这几个核心库:
- EntityFramework 6.4.4:数据访问层的基础,配合Code First模式使用
- PagedList.Mvc:列表页分页,比手写分页逻辑方便得多
- Newtonsoft.Json:JSON序列化,用于Ajax请求
- EPPlus:Excel导入导出,做报表功能的时候用的
创建完成后,我习惯先把目录层级搭出来,再写业务代码。顺序是:定义数据模型(Model)→ 配置EF映射(Fluent API或DataAnnotations)→ 生成数据库 → 编写仓储类 → 编写业务逻辑层 → 编写控制器与视图。
3.2 用Code First还是Database First
EF支持三种开发模式:Code First、Database First、Model First。仓储管理系统我推荐Code First,原因有三个:
- 整个数据模型用C#类定义,代码就是数据库的"唯一事实来源",不会出现类字段和数据库字段不同步的问题。
- 配合EF Migrations(自动迁移),改模型后执行一条命令就能更新表结构,不用手动去数据库里改表。
- 仓库管理系统的表数量不算多(30张以内),Code First完全可控;如果表数量上百,才需要考虑Database First。
使用自动迁移之前要在Configuration.cs里设置AutomaticMigrationsEnabled = true,然后执行:
Enable-Migrations Add-Migration InitialCreate Update-Database3.3 核心功能:入库单提交的完整实现
入库单提交是仓库管理系统最高频的操作,我以这个功能为例展示完整的代码路径。
ViewModel定义(表现层数据模型):
public class InboundOrderViewModel { public string OrderNo { get; set; } public int SupplierId { get; set; } public int WarehouseId { get; set; } public string Remark { get; set; } public List<InboundItemViewModel> Items { get; set; } } public class InboundItemViewModel { public int ProductId { get; set; } public int Quantity { get; set; } public decimal Price { get; set; } public string BatchNo { get; set; } public DateTime? ProductionDate { get; set; } public DateTime? ExpiryDate { get; set; } }Controller端:
[HttpPost] [ValidateAntiForgeryToken] public async Task<ActionResult> Create(InboundOrderViewModel model) { if (!ModelState.IsValid) return View(model); var result = await _inboundManager.CreateInboundOrderAsync(model); if (!result.Success) { ModelState.AddModelError("", result.ErrorMessage); return View(model); } return RedirectToAction("Detail", new { id = result.OrderId }); }BLL核心逻辑:
public async Task<OperationResult> CreateInboundOrderAsync(InboundOrderViewModel model) { try { using (var transaction = _db.Database.BeginTransaction()) { var orderNo = await GenerateOrderNoAsync("IN"); var order = new InboundOrder { OrderNo = orderNo, SupplierId = model.SupplierId, WarehouseId = model.WarehouseId, InboundType = InboundType.Purchase, CreateTime = DateTime.Now, Remark = model.Remark }; _db.InboundOrders.Add(order); await _db.SaveChangesAsync(); decimal totalAmount = 0; foreach (var item in model.Items) { var detail = new InboundOrderDetail { InboundOrderId = order.Id, ProductId = item.ProductId, Quantity = item.Quantity, Price = item.Price, Amount = item.Quantity * item.Price, BatchNo = item.BatchNo, ProductionDate = item.ProductionDate, ExpiryDate = item.ExpiryDate }; _db.InboundOrderDetails.Add(detail); totalAmount += detail.Amount; await UpdateStockAsync(item, order.WarehouseId); } order.TotalAmount = totalAmount; await _db.SaveChangesAsync(); transaction.Commit(); return OperationResult.Success(order.Id); } } catch (Exception ex) { return OperationResult.Fail(ex.Message); } }GenerateOrderNoAsync生成单据编号的逻辑值得说明一下:格式是"IN-20240513-0001",用了日期+当日流水号。流水号不能简单用SELECT COUNT(*) + 1生成,因为高并发下会产生重复。我的做法是在数据库里维护一张OrderSequence表,每次生成编号时用事务更新该表,保证流水号唯一:
public async Task<string> GenerateOrderNoAsync(string prefix) { var dateStr = DateTime.Now.ToString("yyyyMMdd"); // 通过存储过程或者原子性UPDATE获取自增序号 var entity = await _db.OrderSequences .FirstOrDefaultAsync(s => s.Prefix == prefix && s.DateStr == dateStr); if (entity == null) { entity = new OrderSequence { Prefix = prefix, DateStr = dateStr, Seq = 1 }; _db.OrderSequences.Add(entity); } else { entity.Seq++; } await _db.SaveChangesAsync(); return $"{prefix}-{dateStr}-{entity.Seq.ToString("D4")}"; }如果并发量特别大,这个方案可能会有并发冲突,需要用
rowlock或存储过程的原子UPDATE来优化。仓库管理系统的单量一般每天几十到几百单,这个方案实测够用。
4. 常见问题与排查技巧实录
4.1 "检测到有潜在危险的 Request.Querystring 值"怎么破
搜索"webconfig检测到有潜在危险的 request.querystring 值"的朋友应该都遇到过这个问题。这个错误的本质是ASP.NET的请求验证机制检测到URL中的QueryString包含HTML标签(如<,>),默认会拒绝请求以防XSS攻击。
有两种常见场景:
场景一:某个查询功能,用户输入了"<5"这样的筛选条件,URL变成/Product/Search?keyword=<5,直接报错。正确处理方式是过滤用户输入,在URL传参前用Url.Encode编码,或者在后端Trim掉危险字符。
场景二:真的需要让用户传带HTML的内容(比如富文本编辑器),才考虑针对性配置:
<system.web> <!-- 3.5/4.x 全局关闭,不推荐 --> <httpRuntime requestValidationMode="2.0" /> <pages validateRequest="false" /> </system.web>我的建议是永远不要全局关闭,这个机制是保护系统的最后一道防线。正确的做法是使用[ValidateInput(false)]只在需要的Action上关闭,并且确保视图层使用Html.Encode输出。
4.2 EF性能问题:N+1查询与笛卡尔爆炸
用EF做列表页最常踩的坑是N+1查询。比如展示入库单列表,每行要显示"供应商名称",如果直接循环访问order.Supplier.Name,就会产生"1次查询列表 + N次查询供应商"的效果,几十行数据就拖垮页面。
解决方法是用Include或者Select显式预加载:
// 错误写法 var list = db.InboundOrders.Where(o => o.CreateTime > start).ToList(); foreach (var order in list) { var name = order.Supplier.Name; } // 正确写法 var list = db.InboundOrders .Include(o => o.Supplier) .Where(o => o.CreateTime > start) .ToList(); // 或者只投影需要的列,避免全表字段传输 var list = db.InboundOrders .Where(o => o.CreateTime > start) .Select(o => new InboundOrderListVM { Id = o.Id, OrderNo = o.OrderNo, SupplierName = o.Supplier.Name, WarehouseName = o.Warehouse.Name, TotalAmount = o.TotalAmount, CreateTime = o.CreateTime }) .ToList();如果是MVC 5 + EF 6组合,还要小心AutomaticMigrateDatabaseInitializer在每次启动时检查数据库版本,性能上会有一点点损耗,生产环境建议关闭自动迁移,改成手动执行Update-Database脚本。
4.3 发布到IIS的几个关键步骤
MVC项目写完执行"发布",会生成一个发布文件夹。部署到Windows Server 2008 R2或2012的IIS时,有四个坑需要提前规避:
- 应用程序池设置:右键应用程序池,选择"设置应用程序池默认属性"→".NET CLR版本"选"v4.0",管线模式选"集成"。如果选错了模式,访问时会报
HTTP 403.14或404错误。 - 缺少ASP.NET MVC程序集:如果服务器上没有安装对应版本的MVC,网站会报"未能加载文件或程序集 System.Web.Mvc"。解决方式有两种,一是安装"Microsoft ASP.NET MVC 运行时"(服务器基本管控得严,都得走申请审批流程);更好的方式是把MVC相关的DLL包含到Bin目录里发布:
- 右键项目 → 添加对System.Web.Mvc、System.Web.Razor、System.Web.WebPages等程序集的引用
- 设置这些引用为"复制本地 = true"
- 这样发出来的Bin目录自带MVC运行环境,服务器不需要额外安装(这也是热词里"MVC 2 - chs可以卸载"对应的正确处理思路)
- 连接字符串:发布后用记事本打开
Web.config,核对数据库连接字符串是不是指向生产环境的SQL Server。MultipleActiveResultSets=true这个参数建议显式写上,EF某些场景下需要多个打开的DataReader。 - 文件权限:如果写日志或者上传文件,需要给应用程序池对应的用户(通常是
IIS_IUSRS)设置目录的写权限。默认IIS用户没有写权限,会出现"对路径的访问被拒绝"异常。
4.4 源代码管理:Git版本控制与提交规范
这一套系统我从第一行代码就开始用Git管理,强烈建议你也这么做。不是为了装样子,而是仓库管理系统的业务逻辑很复杂,改错一个地方可能影响整个库存数据。有了Git,每次改动都有记录,出了问题可以快速定位是哪次提交引入的。
我的分支管理策略很简单:
master分支:只放稳定可发布的版本develop分支:日常开发的主分支feature/*分支:每个功能模块拉一个分支,做完合并回develop
提交信息有固定的格式,比如feat(入库): 添加入库单批量导入功能、fix(出库): 修复FIFO扣减批次选择错误。这样后续查看历史时,一眼就能知道每次提交改了什么。
补充一点:如果担心自己开发的系统源码被公司拿走或者流传出去,可以考虑用代码混淆工具(比如ConfuserEx、Dotfuscator)对发布的DLL做混淆。C#编译后的DLL是IL中间语言,使用dnSpy、ILSpy这类工具很容易反编译出近似源码。混淆后能增加阅读难度,但没法做到绝对无法破解。现在的技术环境下,没有100%安全的保护方案,合规使用才是核心原则。
4.5 网上找到的"免费源代码"怎么评估
市面上的仓库管理系统源码很多,GitHub上有开源的,一些社区里也有人分享免费版本。直接用这些源码要格外谨慎,我拿到一份外部源码后,一定会做四步检查:
- SQL注入检查:搜索源码里是否有字符串拼接SQL的地方,比如
"SELECT * FROM Product WHERE Id=" + id。如果有,这套源码基本不能直接用。 - 默认密码与后门:检查数据库初始化脚本是否存在全系统通用密码,或者某个隐藏的调试Controller(有些作者会留后门)。
- 依赖版本审计:查看项目中引用的第三方库版本,搜索是否存在已知高危漏洞(可以用NuGet的
dotnet list package --vulnerable命令检测)。 - 业务逻辑验证:先用空数据库跑一遍入库、出库、退货的完整流程,检查每次操作的库存变动和日志记录是否符合预期。没有一个正常人会把钱和货交给一套没验证过的系统。
5. 我的几点经验总结
自己从零开发一套仓库管理系统,整个过程远不是"写几个页面、建几张表"那么简单。做下来最大的收获,是把"业务流程"转换成"数据流转"的能力练扎实了。如果你也要做类似的项目,这里有几点建议:
第一,先跑通业务,再谈技术。用Excel先模拟一套完整的入库出库流程,把所有字段、单据类型、异常情况列出来,再动工写代码。很多项目延期不是技术难点,而是业务梳理时漏掉了关键环节。第二,库存变动一定要留日志。哪怕是开发阶段的测试数据,这条约定也要遵守,不然后期排查问题会非常痛苦。第三,学会用EF和SQL Server自己提供的工具——迁移脚本、事件探查器、执行计划,这些工具能省下大把调试时间。
这套系统的完整源代码我整理到了自己的仓库里,包含商品管理、入库、出库、库存盘点、报表统计、用户权限等模块,数据库脚本也一并附上。拿到手之后先按文档跑起来,再根据自己的业务场景改字段、加模块,比从头开始写要省事很多。如果你在看代码的过程中发现问题,也欢迎随时交流,毕竟系统这种东西,永远有优化空间。
本文还有配套的精品资源,点击获取