
简介一套基于 ASP.NET 的进销存管理系统完整源码面向需要学习企业级 Web 业务系统的开发人员、毕业设计学生或中小企业信息化项目参考者。开发环境为 VS2008 SQL2005 DXperience 控件覆盖客户订单、物料需求计划、采购入库与销售出库、领料/退料/委外加工等典型制造型业务流程并配有采购入库统计、委外加工出入库明细、报废单统计等报表模块以及基础数据、系统管理、文件管理、设备管理等功能业务维度较完整。资源共1119个文件以538个 C# 源文件、197个 resx 资源文件、195个 resources 资源文件为主另有45个 dll 运行库、12个 exe 辅助程序、6个 config 配置及数据库 mdf/ldf 文件整体压缩包 22.98MB代码结构与项目组织便于按模块查阅。已有462人学习下载。对于希望快速理解进销存系统模块划分、报表数据来源、权限与日志设计思路的读者这套源码提供了可直接运行的工程骨架和业务样例适合在此基础上做二次开发或学习研究。1. 把 ASP.NET 进销存源码跑起来这套系统到底解决什么问题干过几年企业信息化的工程师应该都有同感进销存是每个传统企业绕不开的刚需但市面上开源的 PHP、Java 版本不少真正基于 ASP.NET WebForms 且结构够老、够稳、能直接拿去改的反而难找。这套 ASP.NET 进销存管理系统源码是一个完整的三层架构工程覆盖采购入库、销售出库、库存查询、往来单位管理和系统用户权限后端用 C# 编写数据库脚本可直接部署到 SQL Server适合拿来改造成内部管理系统也适合 .NET 方向的学生做课程设计。如果你手头正需要一个能跑通全流程的 C# 进销存参考实现或者想研究传统 ASP.NET 项目怎么分层、怎么控制事务和权限这套源码能省下从零搭框架的大把时间。2. 从业务到表结构先看懂这套进销存的骨架2.1 进销存的核心流程和数据流向这套系统的业务逻辑并不复杂但胜在完整。整个流程围绕「商品」这个核心实体展开采购模块把商品从供应商那里买进来形成采购入库单销售模块把商品卖给客户形成销售出库单库存模块实时维护每种商品的数量和金额。三个模块共用一个商品主档不单独建库存副本这是老系统里比较典型的单库存模型。实际跑起来之后你会发现它的业务边界很清晰采购入库增加库存销售出库减少库存其他如退货、调拨这类操作在基础版本里没有单独入口但可以通过红字单据或者直接改出入库单来处理。对于大多数中小企业来说这套模型的粒度已经够用不需要上到复杂的多仓加权平均。2.2 核心数据表设计建表脚本里的门道拿到源码后我建议你先看数据库脚本而不是急着打开 Visual Studio。这套系统的表结构基本围绕业务单据展开表名用途关键字段Product商品主档ProductCode, ProductName, Spec, Unit, PurchasePrice, SalePrice, StockQtySupplier供应商档案SupplierCode, SupplierName, Contact, PhoneCustomer客户档案CustomerCode, CustomerName, Contact, PhonePurchaseOrder采购入库单头OrderNo, SupplierId, OrderDate, Operator, TotalAmountPurchaseOrderDetail采购入库单明细OrderId, ProductId, Quantity, Price, AmountSaleOrder销售出库单头OrderNo, CustomerId, OrderDate, Operator, TotalAmountSaleOrderDetail销售出库单明细OrderId, ProductId, Quantity, Price, AmountSysUser系统用户UserName, Password, RealName, RoleTypeOperateLog操作日志UserId, ActionType, ActionTime, Detail表之间的关联围绕订单头与订单明细展开典型的「一主多从」结构。订单头记录业务发生在什么时间、哪个往来单位、谁操作的订单明细记录每一条商品的数量与金额。Product 表里的 StockQty 字段是当前库存量每次做入库或出库操作系统就在事务里同时更新明细表和商品库存。这里有一个值得学习的设计细节价格字段直接冗余在订单明细里Price, Amount而不是每次都去关联商品表。这么做的好处是历史单据的价格不会被商品主档的调价影响符合财务上「单据落库即定格」的原则。2.3 数据库脚本的执行顺序翻车率最高的环节数据库脚本一般是一个或多个 .sql 文件按顺序执行建库、建表、插入种子数据。我见过不少人因为直接一键执行整个脚本导致外键约束报错原因就是建表顺序不对——先建了子表而父表还不存在。-- 正确的执行顺序先建主档表再建单据表 CREATE TABLE dbo.Product ( ProductId INT IDENTITY(1,1) PRIMARY KEY, ProductCode NVARCHAR(50) NOT NULL UNIQUE, ProductName NVARCHAR(100) NOT NULL, Spec NVARCHAR(100) NULL, Unit NVARCHAR(20) NULL, PurchasePrice DECIMAL(18,2) DEFAULT 0, SalePrice DECIMAL(18,2) DEFAULT 0, StockQty DECIMAL(18,2) DEFAULT 0, CreateTime DATETIME DEFAULT GETDATE() ); CREATE TABLE dbo.PurchaseOrder ( OrderId INT IDENTITY(1,1) PRIMARY KEY, OrderNo NVARCHAR(30) NOT NULL UNIQUE, SupplierId INT NULL, OrderDate DATETIME DEFAULT GETDATE(), Operator NVARCHAR(50) NULL, TotalAmount DECIMAL(18,2) DEFAULT 0, CONSTRAINT FK_PurchaseOrder_Supplier FOREIGN KEY (SupplierId) REFERENCES dbo.Supplier(SupplierId) );日志StockQty字段用的是DECIMAL(18,2)而不是 INT这是做进销存的一个习惯有些商品按重量或长度计价比如钢材按吨、布料按米整数库存会卡死这类业务。DECIMAL(18,2)足够容纳绝大多数场景而且避免浮点误差。OrderNo字段设置唯一约束也很关键防止并发时生成重复单号。3. 部署这套源码从 IIS 到数据库连接的完整步骤3.1 环境选型老项目有老项目的脾气这套源码基于 ASP.NET WebForms运行环境是 .NET Framework 4.x 或 3.5不是 .NET Core。如果你机器上只装了 ASP.NET Core 运行时直接发布是跑不起来的。建议环境如下组件推荐版本说明操作系统Windows Server 2016 / Win10 / Win11家庭版也能跑无需 ServerIISIIS 7.5 及以上需启用 ASP.NET 功能.NET Framework4.5 / 4.6 / 4.7对应 ASP.NET 版本数据库SQL Server 2008 R2 / 2012 / 2016 / 2019Express 版足够Visual Studio2015 / 2017 / 2019用于打开源码调试数据库管理工具SSMS 18 及以上执行脚本、查数据这里有个常见的坑很多人用 VS2022 打开老工程默认的 MSBuild 版本和工具集不一样可能会提示「需要 .NET Framework 3.5」。如果你的源工程是 3.5 的需要去「控制面板 → 启用或关闭 Windows 功能」里勾选 .NET Framework 3.5包括 2.0 和 3.0或者用 NuGet 把目标框架改成 4.x。我一般直接改成 4.6兼容性最省心。3.2 发布到 IIS 的过程与连接字符串写法源码工程里有 Web.config数据库连接字符串在connectionStrings节点里。老项目最常见的是写死在代码里或者使用 Windows 身份验证而开发环境的库名和服务器名通常跟生产不一致需要手动改。connectionStrings add nameconnStr connectionStringData Sourcelocalhost;Initial CatalogInventoryDB;User IDsa;Passwordyourpassword;MultipleActiveResultSetsTrue providerNameSystem.Data.SqlClient / /connectionStrings把Data Source改成你的 SQL Server 实例名。如果是本机默认实例写localhost或.都可以如果是命名实例写成localhost\SQLEXPRESS。Initial Catalog对应你建的数据库名通常在脚本里能看出来。User ID和Password用sa账号最省事但如果你用的是 Windows 身份验证可以把这两项去掉然后加上Integrated SecurityTrue。发布到 IIS 的时候有一个必经步骤右键项目 → 发布 → 选择「文件系统」把发布后的文件放到一个目录比如C:\inetpub\wwwroot\InventorySys然后在 IIS 管理器里新建网站或应用物理路径指向这个目录应用程序池选「集成」模式。这里如果选成「经典」模式路由和某些控件事件可能会报 500 错误。3.3 首次启动后先做什么验证启动网站后第一件事不是急着点菜单而是先确认三个东西登录页能不能正常加载、数据库连接是否通、种子数据里有没有预设的管理员账号。如果登录页直接报错十有八九是数据库连接问题去 Windows 事件查看器里看 .NET 的异常日志或者临时在 Global.asax 的 Application_Error 里打日志。protected void Application_Error(object sender, EventArgs e) { Exception ex Server.GetLastError(); // 写日志到文件便于定位问题 System.IO.File.AppendAllText( Server.MapPath(~/App_Data/Error.log), DateTime.Now | ex.Message | ex.StackTrace Environment.NewLine ); }这段是放在 Global.asax.cs 里的捕获全局未处理异常写入 App_Data 目录下的日志文件。App_Data目录是 ASP.NET 的既有约定运行账户默认对它有读写权限用在这里最省事。注意要提前确认目录存在否则AppendAllText会抛找不到路径的异常。4. 关键模块实战拆解采购入库、销售出库和库存的联动4.1 采购入库的代码路径事务在这里是关键采购入库页面通常是一个 GridView 列表加上一个详情录入区选供应商、选商品、填数量单价、保存。保存按钮的后端代码是整个模块的核心它必须在一个数据库事务里完成三件事插入订单头、插入订单明细、更新商品库存。using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); SqlTransaction tran conn.BeginTransaction(); try { // 1. 插入订单头 string sqlHeader INSERT INTO PurchaseOrder(OrderNo, SupplierId, OrderDate, Operator, TotalAmount) VALUES(OrderNo, SupplierId, OrderDate, Operator, TotalAmount); SELECT SCOPE_IDENTITY();; SqlCommand cmdHeader new SqlCommand(sqlHeader, conn, tran); cmdHeader.Parameters.AddWithValue(OrderNo, GenerateOrderNo(PO)); cmdHeader.Parameters.AddWithValue(SupplierId, supplierId); cmdHeader.Parameters.AddWithValue(OrderDate, DateTime.Now); cmdHeader.Parameters.AddWithValue(Operator, currentUserName); cmdHeader.Parameters.AddWithValue(TotalAmount, totalAmount); int headerId Convert.ToInt32(cmdHeader.ExecuteScalar()); // 2. 插入订单明细并更新库存 foreach (GridViewRow row in detailGrid.Rows) { int productId Convert.ToInt32(row.Cells[0].Text); decimal qty Convert.ToDecimal(row.Cells[3].Text); decimal price Convert.ToDecimal(row.Cells[4].Text); string sqlDetail INSERT INTO PurchaseOrderDetail(OrderId, ProductId, Quantity, Price, Amount) VALUES(OrderId, ProductId, Quantity, Price, Amount); UPDATE Product SET StockQty StockQty Quantity WHERE ProductId ProductId;; SqlCommand cmdDetail new SqlCommand(sqlDetail, conn, tran); cmdDetail.Parameters.AddWithValue(OrderId, headerId); cmdDetail.Parameters.AddWithValue(ProductId, productId); cmdDetail.Parameters.AddWithValue(Quantity, qty); cmdDetail.Parameters.AddWithValue(Price, price); cmdDetail.Parameters.AddWithValue(Amount, qty * price); cmdDetail.ExecuteNonQuery(); } tran.Commit(); } catch (Exception ex) { tran.Rollback(); // 记日志并提示用户保存失败 } }这段代码的要点是SqlTransaction对象绑定了所有命令任何一个明细插入失败都会回滚整个单据不会出现「单头有了、库存却没变」的脏数据。SCOPE_IDENTITY()拿到的自增主键不需要额外查询直接把订单头和明细关联起来了。这里我想专门提醒一点AddWithValue在这套源码里到处都是。它能跑但如果你追求更好的实践建议改成cmdHeader.Parameters.Add(OrderNo, SqlDbType.NVarChar, 30).Value orderNo这样显式指定类型。AddWithValue 在某些情况下会把参数推断成错误类型导致索引失效或类型转换异常我后面在避坑章节会详细展开。4.2 销售出库的库存扣减并发场景下的隐患销售出库的代码路径和采购入库几乎是对称的区别只在于商品流的成本取的是商品主档里的SalePrice而StockQty的更新是减法。看起来很简单但这里藏着一个并发问题两个人同时卖同一件商品各自读到库存为 10各自减 1最终库存变成 9 而不是 8。-- 不安全的写法 UPDATE Product SET StockQty StockQty - 1 WHERE ProductId 1; -- 相对安全的写法加条件校验 UPDATE Product SET StockQty StockQty - qty WHERE ProductId productId AND StockQty qty; -- 更稳妥的思路先锁行再更新 BEGIN TRANSACTION; SELECT StockQty FROM Product WITH (UPDLOCK, ROWLOCK) WHERE ProductId productId; -- 在应用代码里判断库存是否足够然后执行 UPDATE COMMIT;这套源码里用的是第一种写法对单机部署、单人操作场景没有大问题但如果你要把它部署到门店收银这种并发量高的场景必须改成后两种。UPDATE ... WHERE StockQty qty这个写法利用 SQL Server 的行锁机制当第二个请求进来时如果库存已经在第一个事务里被扣减到不足UPDATE影响的行数为 0代码里再判断ExecuteNonQuery()的返回值即可提示用户库存不足。4.3 订单编号生成别用 GUID用可读的序列号这套系统里的单号生成逻辑值得单独拿出来说。默认是按日期加随机数生成比如PO20241226001。用 GUID 做订单号虽然省事但打印在单据上用户根本不认财务对账也麻烦。日期加序号的方式在用户侧是最好沟通的但要注意并发重复的问题。private static string GenerateOrderNo(string prefix) { // 按天归零的计数器配合锁避免并发重复 lock (_orderLock) { if (_lastGenerateDate ! DateTime.Today) { _dailySeq 0; _lastGenerateDate DateTime.Today; } _dailySeq; return string.Format({0}{1:yyyyMMdd}{2:D3}, prefix, DateTime.Today, _dailySeq); } }这段逻辑用静态变量记录当天的日期和序列号每天零点是 1每生成一单加 1。lock保证同一进程内不会产生并发重复。但如果你的应用跑在 Web 集群里多个 IIS 进程同时工作就需要把序列号存到数据库表里否则多个进程各自计数还是会重复。5. 避坑指南部署和改造这套源码最容易翻车的五个坑5.1 翻车现象登录页 500 错误事件日志里报「找不到 SqlClient 程序集」登录页打不开IIS 直接抛 500。遇到这种情况先看 Windows 事件查看器常见的是「Could not load file or assembly System.Data.SqlClient」或者「FileNotFoundException」。原因其实是老项目的Bin目录里缺少依赖的程序集文件或者 .NET Framework 版本不匹配。很多网上流传的源码工程只给了 .aspx 和 .cs 文件Bin 目录是空的而 WebForms 项目引用的第三方组件没有随源码一起打包。解决方法是先把项目在 Visual Studio 里手动编译一次右键项目 → 生成确认生成成功后再发布。如果编译都报错优先检查目标框架版本和 NuGet 包还原。用 VS 打开项目后在解决方案资源管理器右键解决方案 → 还原 NuGet 程序包绝大多数缺程序集的问题到这里都能解决。5.2 翻车现象Session 频繁丢失登录后隔几分钟就被踢回登录页用户登录后没操作多久再点页面就跳到登录页需要重新登录。原因是 ASP.NET 默认的 Session 存储在进程内InProcIIS 应用程序池如果配置了「回收工作进程」或者进程因为内存超限被系统重启Session 就没了。另一个原因是 Web.config 里 SessionState 配置写成了InProc且超时时间过短。解决方法是看 Web.config 里的system.web节点把 Session 超时调大system.web sessionState modeInProc timeout120 / /system.web如果确认回收策略导致频繁丢失更彻底的办法是把 Session 改成StateServer或SQLServer模式后者需要额外建数据库表配置起来稍复杂但在生产环境更稳。判断当前模式的方法很简单打开多个浏览器标签页登录后刷新如果某些标签页掉线而另一些还活着基本就是 InProc 模式下的进程回收导致的。5.3 翻车现象数据中文全部变成问号页面显示正常但写入 SQL Server 数据库的中文全部变成???尤其是订单备注、客户名称这类字段。原因是数据库表的字符集不是NVARCHAR系列。老脚本里如果把表字段定义成VARCHAR而代码里传入的是中文字符串客户端与服务器之间的字符编码不一致时就会变成问号。SQL Server 的VARCHAR只能存单字节字符NVARCHAR才能存 Unicode 字符。解决方法是检查脚本里的建表语句把涉及中文的字段改成NVARCHAR(50)ALTER TABLE dbo.Customer ALTER COLUMN CustomerName NVARCHAR(100) NOT NULL;跑一遍这个语句后再重新插入测试数据。需要注意的是即使改了字段类型之前已经变成问号的历史数据无法通过ALTER恢复只能重新录入。所以换字段类型前先确认表的业务数据状态是测试数据就直接清掉是正式数据就得先导出备份再改。5.4 翻车现象导出 Excel 报错提示「检索 COM 类工厂中 CLSID 为 00024500-0000-0000-C000-000000000046 的组件失败」很多进销存系统的报表功能用到了 Excel 导出代码里直接用了Microsoft.Office.Interop.Excel部署到服务器后点击导出按钮报 COM 组件错误。原因是服务器上没装 Office或者装了但没配置正确的 DCOM 权限。Interop 方式要求服务器安装完整版 Office这在生产服务器上既不安全也不太现实。解决方法是把导出功能改用轻量级的方案比如用后端生成 CSV 文件流或使用开源库如 NPOI 直接生成 .xlsx 文件using NPOI.XSSF.UserModel; using System.IO; // 生成 Excel 文件流返回给前端 var workbook new XSSFWorkbook(); var sheet workbook.CreateSheet(库存报表); sheet.CreateRow(0).CreateCell(0).SetCellValue(商品编码); sheet.CreateRow(0).CreateCell(1).SetCellValue(商品名称); // ... 填充数据行 using (var ms new MemoryStream()) { workbook.Write(ms); Response.Clear(); Response.ContentType application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; Response.AddHeader(Content-Disposition, attachment; filenameinventory.xlsx); Response.BinaryWrite(ms.ToArray()); Response.End(); }NPOI 通过 NuGet 安装它不依赖服务器上的 Office 安装任何组件断网环境也能用生成的文件是标准 xlsx 格式完美替代 Interop。改完之后记得把原来 Interop 的using语句全部删除否则编译时会报引用缺失。5.5 翻车现象输入框输入特殊字符导致页面整套报错在商品名称、客户备注等输入框里输入类似 script这类字符点保存后页面直接报数据库语法错误或者直接弹出一个黄色的运行时错误页。原因是系统没有做参数化的地方直接用字符串拼接 SQL 造成的。老项目中常见这种写法// 错误示例直接拼接 SQL string sql INSERT INTO Customer(CustomerName) VALUES( txtName.Text );这既是 SQL 注入风险也解释了为什么输入时会报错。解决方法是把所有数据库操作统一改成参数化查询string sql INSERT INTO Customer(CustomerName) VALUES(name); SqlCommand cmd new SqlCommand(sql, conn); cmd.Parameters.AddWithValue(name, txtName.Text.Trim());这段代码的参数化写法能直接屏蔽掉特殊字符带来的语法问题也堵住了注入入口。把源码里所有sql拼接模式挨个过一遍虽然费时间但强烈建议不要跳过——我拆过很多老项目这里的问题比看着多得多。6. 进阶玩法给这套系统加一个库存预警功能整个系统跑通之后最值得先做的一个小改造是库存预警。具体思路是给 Product 表加两个字段MinStockQty最低库存上限和MaxStockQty最高库存上限然后在系统首页展示一个待补货商品列表用颜色区分超储和欠储。这个需求在几乎每家进销存使用方那里都会被提出来你提前做好以后二次开发就能直接复用。第一步加字段ALTER TABLE dbo.Product ADD MinStockQty DECIMAL(18,2) DEFAULT 0; ALTER TABLE dbo.Product ADD MaxStockQty DECIMAL(18,2) DEFAULT 0;第二步在库存查询页面的 GridView 里加一个状态列用 C# 代码判断当前库存与上下限的关系protected string GetStockStatus(decimal stockQty, decimal minQty, decimal maxQty) { if (stockQty minQty) return 警示; if (stockQty maxQty) return 超储; return 正常; }第三步写一个简单的后台数据访问方法把预警数据绑定到首页的 Repeater 控件上public DataTable GetWarningProducts() { string sql SELECT ProductCode, ProductName, StockQty, MinStockQty, CASE WHEN StockQty MinStockQty THEN 库存不足 WHEN StockQty MaxStockQty THEN 库存积压 ELSE 正常 END AS WarningType FROM Product; // 执行查询并返回 DataTable }这套改造十几分钟就能完成但它让源码从一个「会记流水账的系统」升级成「有判断能力的管理工具」。从那以后我每次拿到老项目源码都会习惯性地先给核心表加上预警字段再谈别的这个习惯帮我在后续项目里少踩了很多次「业务部门不知道库存什么时候断货」的坑。希望这轮分享能帮你顺利把这套 ASP.NET 进销存系统跑起来并且跑得比原版更顺手。本文还有配套的精品资源点击获取