资讯详情

资讯详情

ASP.NET进销存ERP源码实战:单据驱动库存与并发处理

简介这是一套基于ASP.NET的ERP进销存管理系统源码面向具备一定.NET基础的开发者与中小企业信息化实施人员可用于学习企业级进销存业务逻辑或作为二次开发与课程设计的参考项目。资源包共约2000个文件压缩后27.33MB其中351个aspx页面承载各业务模块界面460个cs文件实现后台逻辑802个js与120个css负责前端交互与样式另有42个dll、6个config及若干db、mdf、ldf数据库文件整体结构完整。开发环境为VisualStudio2010搭配SqlServer2008R2基于.NET4.0数据库文件位于DB_sq文件夹附加后修改web.config中的连接字符串即可运行管理员账号密码为8001。目前已有243人学习下载适合通过阅读源码理解进销存系统的分层设计、页面组织与数据访问方式并在此基础上进行功能扩展与调试实践。1. 一套 ASP.NET 进销存 ERP 源码到底能省掉哪些重复造轮子的活接手过几个中小型贸易公司的数字化项目后我对「从零写一套进销存」这件事已经彻底祛魅。采购、销售、库存、往来账、报表这五块业务逻辑在十年里几乎没变过变的只是前端框架和部署方式。所以当有人问我 ASP.NET 的 ERP 管理系统源码、进销存管理系统源码值不值得拿来用我的回答通常是看你缺的是业务骨架还是业务细节。这类源码真正值钱的地方不是它写了多少行代码而是它把「单据驱动库存」这套模型跑通了——采购单审核后自动生成入库单入库单过账后库存数量、批次、成本价同步变动销售出库再反向冲减。这套链路自己从零搭光是把并发扣减和负库存拦住就够折腾两三周。这篇文章面向两类人一类是接了小商贸公司私活、想快速交付一套能用的进销存系统的独立开发者另一类是公司内部要做轻量 ERP、又不想买商业软件的实施或全栈工程师。我会按「拿到源码后先看什么 → 数据库和核心单据怎么落地 → 权限与并发怎么处理 → 部署和踩坑」的顺序讲中间给可抄的建表语句、过账逻辑和参数配置。ASP.NET 这条技术栈老项目多是 WebForms 或 MVC5新一点的用 ASP.NET Core MVC两者在源码结构上差别不小选型那节会专门说清楚。2. 拿到源码先别急着跑从项目结构判断它值不值得改2.1 先分清 WebForms、MVC5 和 ASP.NET Core MVC 三种骨架市面上流通的 ASP.NET 进销存源码按年代大致分三档。第一档是 WebForms特征是 .aspx 页面配 .aspx.cs 后台代码控件拖拽、ViewState 满天飞优点是上手快缺点是前后端耦合死改一个列表页要动三四个文件。第二档是 MVC5 EF6有 Controller、View、Model 分层路由清晰是过去十年中小项目的主力。第三档是 ASP.NET Core MVC跨平台、依赖注入原生支持、EF Core 做数据访问是目前还在维护的源码主流。判断方法很直接打开解决方案文件看项目引用。如果看到System.Web.dll、System.Web.Mvc这类引用基本是 .NET Framework 时代的产物只能跑在 Windows IIS 上。如果看到Microsoft.AspNetCore.App这种框架引用或者 csproj 里写着TargetFrameworknet6.0/TargetFramework、net8.0那就是 Core 版本能上 Linux。这一步决定了你后面部署的自由度别等写完业务才发现只能绑死在 Windows Server 上。提示如果源码里同时出现System.Web和Microsoft.AspNetCore多半是迁移到一半的混合项目这种最坑改造成本可能比重写还高接手前先评估。2.2 用一张表快速体检源码的完整度拿到一套源码我一般花半小时做一次「功能体检」看它到底覆盖了多少进销存的核心环节。下面这张表是我常用的检查清单缺得越多你要补的活就越多。模块必须有的能力缺失后的补救成本基础资料商品、客户、供应商、仓库、计量单位低自己补几张表采购管理采购订单、采购入库、采购退货中涉及库存和应付销售管理销售订单、销售出库、销售退货中涉及库存和应收库存管理即时库存、库存流水、盘点、调拨高是整套系统的核心往来账应收应付、收付款单、对账单中逻辑独立报表进销存汇总、毛利、库存预警低到中看复杂度权限角色、菜单、数据权限中数据权限最难如果库存流水和往来账这两块是空的那这套源码基本只能当 UI 模板用业务价值大打折扣。反过来如果这两块齐全哪怕前端丑一点也值得花时间改。2.3 跑起来的最小步骤数据库脚本 连接串 初始账号体检完就该让它跑起来。以 ASP.NET Core MVC 版本为例最小启动路径是三步。第一步找到数据库脚本通常在Database或Scripts目录下扩展名是 .sql。第二步改连接字符串位置在appsettings.json。第三步还原 NuGet 包并启动。# 1. 建库并导入脚本以 SQL Server 为例 sqlcmd -S localhost -U sa -P YourPassword -Q CREATE DATABASE ErpDb sqlcmd -S localhost -U sa -P YourPassword -d ErpDb -i ./Database/init.sql # 2. 还原依赖并编译 dotnet restore dotnet build # 3. 启动默认监听 5000 端口 dotnet run --urls http://0.0.0.0:5000appsettings.json里的连接串是第一个要改的地方格式如下。TrustServerCertificateTrue在本地自签证书环境下必须加否则 SQL Server 会直接拒绝连接这是新手最常卡的一步。{ ConnectionStrings: { Default: Serverlocalhost;DatabaseErpDb;User Idsa;PasswordYourPassword;TrustServerCertificateTrue; } }初始账号一般在脚本末尾的INSERT INTO Sys_User语句里密码多是 MD5 或 SHA256 哈希。如果登录报错先确认哈希算法和代码里VerifyPassword的实现是否一致很多源码在这里埋了坑——脚本里的哈希和代码用的算法对不上导致默认账号永远登不进去。3. 单据驱动库存进销存的核心模型怎么落地3.1 库存不是一张表而是「余额表 流水表」双写新手最容易犯的错是只建一张Stock表存当前数量出库就减、入库就加。这样做的后果是一旦数量对不上你根本查不出是哪一笔单据出的问题。正确做法是双表结构——StockBalance存每个商品在每个仓库的当前结存StockFlow存每一笔引起库存变动的流水。任何一次库存变动都要在同一个事务里同时写这两张表。-- 库存余额表商品 仓库 维度唯一 CREATE TABLE StockBalance ( Id BIGINT IDENTITY PRIMARY KEY, GoodsId INT NOT NULL, WarehouseId INT NOT NULL, Qty DECIMAL(18,4) NOT NULL DEFAULT 0, -- 当前结存数量 CostPrice DECIMAL(18,4) NOT NULL DEFAULT 0, -- 移动加权成本 RowVersion ROWVERSION, -- 并发控制用 CONSTRAINT UQ_Balance UNIQUE (GoodsId, WarehouseId) ); -- 库存流水表只增不改作为对账依据 CREATE TABLE StockFlow ( Id BIGINT IDENTITY PRIMARY KEY, GoodsId INT NOT NULL, WarehouseId INT NOT NULL, BizType TINYINT NOT NULL, -- 1采购入库 2销售出库 3盘点 4调拨 BizNo VARCHAR(32) NOT NULL, -- 来源单据号 ChangeQty DECIMAL(18,4) NOT NULL,-- 正数入库 负数出库 AfterQty DECIMAL(18,4) NOT NULL,-- 变动后结存便于追溯 CreateTime DATETIME2 NOT NULL DEFAULT SYSDATETIME() );RowVersion这一列是 SQL Server 的行版本戳EF Core 里配成并发令牌后两个用户同时改同一条余额记录时后提交的那个会抛DbUpdateConcurrencyException逼你重试。这比用悲观锁简单也比不锁安全。AfterQty冗余存变动后结存是为了对账时不用从头累加流水直接看最后一笔就知道当时库存是多少。3.2 过账逻辑一个事务里完成余额更新和流水写入单据「过账」是进销存里最关键的动词意思是这张单据正式生效、影响库存和账务。下面这段是采购入库过账的核心逻辑用 EF Core 写重点看事务边界和并发处理。public async Task PostInboundAsync(long inboundId) { using var tx await _db.Database.BeginTransactionAsync(); try { var order await _db.InboundOrders .Include(o o.Items) .FirstAsync(o o.Id inboundId); if (order.Status OrderStatus.Posted) throw new BizException(单据已过账不能重复操作); foreach (var item in order.Items) { // 1. 取余额行带并发令牌 var balance await _db.StockBalances .FirstOrDefaultAsync(b b.GoodsId item.GoodsId b.WarehouseId order.WarehouseId); if (balance null) { balance new StockBalance { GoodsId item.GoodsId, WarehouseId order.WarehouseId, Qty 0, CostPrice 0 }; _db.StockBalances.Add(balance); } // 2. 移动加权平均重算成本价 var oldAmount balance.Qty * balance.CostPrice; var newAmount item.Qty * item.Price; var newQty balance.Qty item.Qty; balance.CostPrice newQty 0 ? 0 : (oldAmount newAmount) / newQty; balance.Qty newQty; // 3. 写流水AfterQty 记录变动后结存 _db.StockFlows.Add(new StockFlow { GoodsId item.GoodsId, WarehouseId order.WarehouseId, BizType 1, BizNo order.OrderNo, ChangeQty item.Qty, AfterQty newQty }); } order.Status OrderStatus.Posted; order.PostedTime DateTime.Now; await _db.SaveChangesAsync(); await tx.CommitAsync(); } catch { await tx.RollbackAsync(); throw; } }逻辑说明整个过账在一个事务里完成任何一步失败全部回滚保证余额和流水不会出现「加了一半」的脏数据。移动加权平均的公式是(原库存金额 本次入库金额) / (原数量 本次数量)这是商贸公司最常用的成本核算方式比先进先出简单比固定成本准确。参数说明item.Price是入库单价注意区分含税价和不含税价很多源码在这里没做区分导致成本虚高。OrderStatus.Posted是枚举值过账前必须校验状态防止重复过账把库存加两遍——这是血泪教训我见过因为没校验状态一张单子被点了三次库存直接翻三倍。3.3 出库时的负库存拦截与成本回写销售出库和采购入库方向相反但多了两个判断一是库存够不够二是出库成本按当前移动加权价回写。库存不足时是拦截还是允许负库存取决于业务。零售场景一般允许负库存先卖后补但批发和制造必须拦截。// 出库前校验AllowNegativeStock 从配置读取 if (!_options.AllowNegativeStock balance.Qty item.Qty) throw new BizException($商品 {item.GoodsName} 库存不足当前 {balance.Qty}); // 出库成本 当前移动加权价直接回写到销售单明细 item.CostPrice balance.CostPrice; balance.Qty - item.Qty; // 出库不改变 CostPrice只减数量AllowNegativeStock这个开关建议做成系统参数放在Sys_Config表里不同仓库可以配不同策略。出库成本回写很重要它决定了毛利报表准不准。如果出库时不记录成本价事后算毛利只能拿当前成本倒推库存价一变历史毛利全乱。4. 权限、并发和那些让系统半夜报警的坑4.1 数据权限比功能权限难十倍功能权限好做角色绑菜单登录时把菜单树查出来渲染就行。真正难的是数据权限——张三只能看自己负责的客户李四只能看本部门的单据。常见做法是在业务表上加OwnerId或DeptId字段查询时统一拼过滤条件。// 在 DbContext 或仓储层统一注入数据权限过滤 public IQueryableSaleOrder ApplyDataPermission(IQueryableSaleOrder query) { var user _currentUser; if (user.IsAdmin) return query; // 按部门过滤递归包含子部门 var deptIds _deptService.GetSelfAndChildren(user.DeptId); return query.Where(o deptIds.Contains(o.DeptId)); }关键是这个过滤要统一入口不能每个 Controller 各写各的否则漏一个地方就是数据泄露。我一般把它放在仓储基类里所有查询默认走一遍。GetSelfAndChildren用递归 CTE 查部门树SQL Server 里用WITH ... AS写别在内存里递归部门一多性能就崩。4.2 并发扣库存的三种处理方式对比高并发下扣库存是经典问题。下面这张表是我实际用过的三种方案按场景选。方案实现方式适用场景代价乐观锁RowVersion 并发令牌并发量中等、冲突少冲突时抛异常需重试悲观锁SELECT ... WITH (UPDLOCK)冲突频繁、单据量小锁等待吞吐低队列串行消息队列按商品分区秒杀级高并发架构复杂一致性延迟进销存系统绝大多数场景用乐观锁就够了因为同一商品同一仓库被两个人同时过账的概率并不高。真到了电商大促那种量级也不是靠数据库锁解决的而是靠预扣库存 异步落库。别一上来就上队列那是过度设计。4.3 避坑清单五个让我加过班的真实问题现象一单据过账后库存没变。原因多半是事务提交了但SaveChangesAsync没被调用或者过账方法里改了实体却没加入跟踪。解决确认实体是通过_db查出来的被跟踪而不是new出来的在事务提交前显式调用一次SaveChangesAsync。现象二库存数量对但成本价是负数。原因是出库时用了错误的成本价或者移动加权公式里除数为零没处理。解决在重算成本价前判断newQty 0直接置零出库成本严格取当前balance.CostPrice不要自己算。现象三两个用户同时过账其中一个报并发异常。这是乐观锁在正常工作不是 bug。解决在业务层捕获DbUpdateConcurrencyException重新加载实体、重放业务逻辑重试两到三次。别直接把这个异常抛给用户看。现象四报表数据和库存余额对不上。八成是有人直接改了余额表或者历史数据迁移时流水没补全。解决写一个对账脚本按StockFlow累加每个商品每个仓库的ChangeQty和StockBalance.Qty比对不一致的列出来人工核。这个脚本建议做成定时任务每天跑一次。现象五部署到 Linux 后日期格式全乱。ASP.NET Core 在 Linux 上默认用 ICU 的全球化数据DateTime.ToString()的输出和 Windows 不一致。解决在Program.cs里设置CultureInfo.DefaultThreadCurrentCulture new CultureInfo(zh-CN)或者干脆在 csproj 里加InvariantGlobalizationtrue/InvariantGlobalization关掉全球化日期统一用 ISO 格式传输。5. 从能跑到好用报表优化和二次开发的取舍5.1 进销存报表慢先看索引再谈缓存进销存汇总报表是性能重灾区尤其是「某时间段某仓库所有商品的期初、入库、出库、期末」这种。很多人第一反应是加 Redis 缓存但十有八九是索引没建对。StockFlow表上至少要有一个覆盖索引(WarehouseId, GoodsId, CreateTime)包含ChangeQty。这样按仓库和时间范围聚合时能走索引扫描不用回表。CREATE NONCLUSTERED INDEX IX_StockFlow_Report ON StockFlow (WarehouseId, GoodsId, CreateTime) INCLUDE (ChangeQty, BizType);建完索引再测如果还慢再看是不是查询写法有问题——比如在 WHERE 里对CreateTime用了函数CONVERT(varchar, CreateTime, 112) 20240101这会让索引失效。改成范围查询CreateTime 2024-01-01 AND CreateTime 2024-01-02性能立刻不一样。缓存是最后手段不是第一手段。5.2 二次开发前先想清楚改源码还是加扩展拿到一套源码最忌讳的是直接冲进去改核心逻辑。我的习惯是能在扩展点解决的绝不改核心。ASP.NET Core 的依赖注入和中间件机制给了很多扩展空间。比如要加一个「过账后发通知」的功能不要去改PostInboundAsync而是定义一个领域事件在过账成功后发布由独立的处理器去发通知。// 过账成功后发布事件不侵入核心逻辑 await _db.SaveChangesAsync(); await tx.CommitAsync(); await _eventBus.PublishAsync(new InboundPostedEvent(order.Id, order.OrderNo));这样核心过账逻辑保持干净通知功能可以随时加、随时删出问题也不影响主流程。如果源码本身没有事件机制那就自己在过账方法末尾加一行发布调用改动最小。判断标准很简单你的改动会不会让「过账」这个动作多一个失败点会就别改核心。5.3 一个我坚持了多年的习惯每次接手一套新源码我都会先写一个「冒烟测试脚本」——用最少的步骤把采购入库、销售出库、库存查询、报表生成这条主链路跑一遍记录每一步的预期结果。这套脚本不追求覆盖率只求在主流程被改坏时第一时间发现。进销存系统的 bug 有个特点它不会立刻报错而是悄悄把数据搞乱等你月底对账才发现。到那时候你根本不知道是哪天哪次改动引入的。有了冒烟脚本每次改完跑一遍心里有底。这个习惯帮我省下的加班时间远比写脚本花的时间多。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →