多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

制造业进销存ERP源码实战:从B/S架构到C#代码部署改造

制造业进销存ERP源码实战:从B/S架构到C#代码部署改造 简介这份源码是基于ASP.NET开发的大型制造业进销存ERP管理系统采用典型的三层架构面向需要构建进销存一体化管理平台的.NET开发人员或制造企业。系统覆盖销售、生产、采购、仓库、结算五大业务域包含合同签订、销货退货、计划申报、产品检验、库存调拨、应收应付等完整流程并支持凭证打印与单据流转可帮助技术人员快速理解企业级业务系统的模块划分与数据关系。资源包共2000个文件大小17.62MB其中1221个gif与254个png提供界面图标与样式素材491个cs为C#核心逻辑与数据访问代码207个aspx为页面文件159个css与155个js完善前端交互与样式另含数据库及Office文档相关组件目录清晰便于按模块检索与二次开发。系统运用ExtJSjQueryAjaxPro实现富客户端无刷新交互借助NPOI完成Excel数据导出并通过OfficeActiveX插件生成Word文档适合作为学习三层架构、Ajax异步交互及办公文档集成技术的实战范例。已有884人学习下载适合需要快速搭建企业进销存原型或扩展业务模块的开发者。1. 大型制造业进销存ERP源码它不是一个压缩包而是一条产线练习场看到“进销存ERP源码下载 asp.net 大型制造业进销存源码 c# 源代码 bs erp 电子版.zip”这个标题多数人的第一反应是先存进网盘然后陷入“解压一天、跑起来无期”的循环。我建议换个视角这个标题真正在问的是一份能落地的B/S版制造业进销存源码应该长什么样拿到之后要做什么才能让它变成一套能用的系统。它解决的核心问题很集中制造业的采购、销售、库存、物料清单BOM、工单领料和完工入库要有一套能在浏览器里跑的ERP而不是一堆Excel在各部门之间来回传。适合做这件事的人主要有三类想给中小制造企业搭内部系统的IT工程师接ERP二次开发订单的外包团队以及用.NET/C#练手大型系统架构的初级开发者。最终要认清一点不要指望下载一个包就能直接上生产。任何成熟老ERP都是“源码数据库脚本业务细节二次开发”四件事一起完成的这个标题只是入口。2. 别急着写代码先拆开B/S进销存ERP的骨架2.1 浏览器里的ERP和C/S时代差在哪里老牌的制造业ERP大多是C/S结构客户端要装一个桌面程序每个操作员机器上都要装框架、配置服务器地址版本更新时还得跑到每一台电脑上重新发布。换成ASP.NET的B/S结构后仓库的扫码枪、办公区的PC、老板的笔记本只需要一个浏览器就能访问。这就是标题里“bs”这两个字母最直接的价值。一个ASP.NET网站的请求流程并不神秘浏览器通过HTTP请求到达IISIIS根据文件扩展名把请求交给ASP.NET程序集进入页面生命周期或者MVC路由。如果你拿到的是WebForms老源码会发现每个aspx页面背后都有一个继承System.Web.UI.Page的类事件都在Page_Load里按IsPostBack分叉如果是MVC则是Controller里返回ActionResult。理解这一点非常重要因为很多“能登录但一点按钮就500”的问题最后都回到这个执行路径上。C/S和B/S最大的差异在于客户端不可控。C/S时代可以在客户端存不少临时状态B/S的应用只能依托Session、Cookie和数据库状态。业内常见的做法是服务端做权限校验、防重复提交、库存校验前端只负责展示。但不少下载回来的源码把判断逻辑写在JavaScript里这就埋了重大隐患懂技术的人绕过前端控件可以直接构造请求把库存打穿。所以拿到源码第一件事不是看界面多不多而是看服务端有没有真正的校验代码。2.2 制造业进销存比商贸系统多了一层“工单状态流”商贸进销存的核心是“进、销、存”一张入库单、一张出库单扣掉库存和加回库存事就完了。制造业不是这样它的产品是由BOM展开的。一个成品可能由几种半成品组成半成品又由多种原材料组成。生产部门下生产工单后要按BOM生成领料单去仓库领料仓库发料后库存减少等工单完工再把成品入库库存增加同时结算材料成本。这个业务流决定了制造业ERP的数据库不能只记“数量”还必须记录“数量变化的原因”。同样是库存减少日常销售退货、生产领料、盘亏、报废对财务和成本的影响完全不一样。普通商贸系统往往只需要一个出入库类型字段就能应付制造业则至少需要一套单据状态机草稿、已审核、领料中、已完工、已关闭。已经审核的单据不能物理删除只能做红冲或负数对冲这也是老源码里常见大量负单的原因。所以说一个标题里写着“大型制造业”的进销存源码如果里面没有BOM表、生产工单、领料/退料逻辑那它本质上就是一套普通进销存跟“制造业”三个字关系不大。评估源码时请先把这些表列出来。2.3 选型WebForms、MVC还是ASP.NET Core拿到一份老电子版源码大概率是.NET Framework时代的WebForms项目类库以App_Code或bin里的DLL形式存在。它的特点是事件驱动写起来快但ViewState很臃肿前端HTML也难控制。如果你只需要在局域网里跑IIS加上WebForms完全够用如果要对外开放或者要跟新业务API对接MVC和ASP.NET Core会舒服很多。我一般会做以下对比来评估运行环境WebForms通常要求Windows Server IIS .NET Framework 4.xASP.NET Core可以跨平台部署Linux Nginx也能跑。学习曲线会写C#都会上手WebForms但不易理解HTTP本质ASP.NET Core的中间件管线透明问题好排查。维护资源老源码的更新资料不好找成熟工程师也常去翻微软官方文档新项目选ASP.NET Core资料多、社区活跃。第三方库老的报表控件、加密控件可能是32位COM组件必须配IIS应用池和对应位的x86新项目用现代NuGet包没有这种限制。如果在“生产可用”和“二次开发成本”之间权衡我倾向于老源码只当跑通业务模型的参考新开发选ASP.NET Core。这个判断会在本文最后一部分展开说明。3. 把“电子版.zip”变成能登录的系统最小操作集与三个必调参数3.1 解压后先做三件事而不是直接双击.sln很多人拿到源码后第一件事是找.sln双击结果VS提示一堆“找不到包”“版本不兼容”。我建议先花 15 – 30 分钟审查包的结构。常见包内至少应有四种证据解决方案文件.sln或.csproj、数据库脚本文件.sql或.bak备份、Web配置文件web.config、以及编译产物目录bin。如果只有网站文件夹而无任何数据库脚本那么这个源码很可能只是“半成品”跑通的机会很低。第二件事是看有没有README、部署说明或版本记录。老ERP源码里经常会写着数据库实例名、初始账号密码、IIS站点配置要求。这些信息藏得很深但不看会踩大坑。第三件事是验证IIS功能是否开启。Windows上需要打开“启用或关闭Windows功能”把IIS、.NET Extensibility、ISAPI扩展、ASP.NET相关项打勾。这一步不做完即便代码正确也会报“处理程序页面未能实现”的错误。3.2 连接字符串最常见的“找不到”和“sa登录被拒”ASP.NET老源码的数据库连接几乎都放在web.config的connectionStrings节点里。拿到包之后建议先把连接字符串全部替换成本地开发库的配置而不是逐个页面去搜。一个常见写法是connectionStrings add nameERPConnectionString connectionStringServer.;DatabaseYL_ERP;User IDerp_user;PasswordYourStrong!Pass;MultipleActiveResultSetstrue;Trusted_Connectionfalse providerNameSystem.Data.SqlClient / /connectionStrings这里有几个参数值得解释。Server.表示本机默认实例如果数据库是命名实例要写成Server.\SQLEXPRESS或Server机器名\实例名。Trusted_Connectionfalse表示使用SQL账号密码登录true表示用Windows集成认证二者只能选其一。在IIS部署时集成认证会沿用应用池身份如果该身份没有数据库权限你会在登录时看到“用户无法登录”或“无法打开数据库”。MultipleActiveResultSetstrue是我踩过坑的参数。很多老代码用一个SqlConnection同时跑多个DataReader或者在一个Reader没关闭的情况下又执行另一条命令不开启MARS会直接抛“已有打开的与此连接相关联的DataReader”。如果源码里大量用DataTable.Fill这种一次性加载这个参数开不开都行但为了少改代码建议保持true。3.3 用IIS Express还是IIS注意应用池和位数开发期用Visual Studio内置的IIS Express很快直接F5就能起来。但IIS Express的请求URL通常是localhost:端口号/应用路径如果源码里有绝对路径比如/ERP/login.aspx就会变成404。这时候要在项目属性里设置“虚拟路径”为/或者改用本地IIS创建虚拟目录并“转换为应用程序”。生产环境部署时应用池有两个常被忽略的开关。一个是.NET CLR版本老WebForms源码要选择“.NET CLR版本 v4.0.30319”选成“无托管代码”会出现一堆扩展名错误。另一个是“启用32位应用程序”如果引用了Access数据库、老版Excel导出组件或32位COM打印控件这个开关必须设为true否则运行时就会报“试图加载格式不正确的程序”。3.4 第一个C#连通性自检排除黑匣子问题在调试任何页面之前我习惯先写一个小函数验证数据库通不通。这不占用太多时间但能把“代码BUG”和“环境问题”分开。下面这段代码可以直接放在项目中临时调用using System; using System.Data.SqlClient; using System.Configuration; public class DbSelfCheck { public static bool TryOpen(out string message) { try { using (SqlConnection conn new SqlConnection( ConfigurationManager.ConnectionStrings[ERPConnectionString].ConnectionString)) { conn.Open(); message 连接成功当前数据库 conn.Database; return true; } } catch (Exception ex) { message ex.Message; return false; } } }这段代码的关键在于把连接字符串从web.config传入而不是在代码里硬编码密码。使用using保证连接被正确关闭。如果返回的异常是“网络相关或实例特定错误”优先检查SQL Server服务有没有启动、端口有没有被防火墙拦截如果是“用户sa登录失败”就要回到SQL Server安全性里检查账号状态并重置密码。4. 从入库单到库存台账C#代码和SQL表的设计闭环4.1 九张表就能撑起小型制造业进销存在评估或改造进销存源码时不要被几十张表吓住。核心业务闭环通常可以用少量表完成商品档案Item、仓库档案Warehouse、库存量Inventory、库存流水InventoryFlow、采购入库单PurchaseIn、销售出库单SaleOut、BOM明细BOMDetail、生产工单WorkOrder以及工单领料/退料记录WorkOrderMaterial。下面是一份常用的建表骨架CREATE TABLE Item ( ItemCode NVARCHAR(32) PRIMARY KEY, ItemName NVARCHAR(100) NOT NULL, Spec NVARCHAR(100) NULL, Unit NVARCHAR(10) NULL, IsProduced BIT NOT NULL DEFAULT 0, DefaultCost DECIMAL(18,4) NOT NULL DEFAULT 0 ); CREATE TABLE Warehouse ( WHCode NVARCHAR(20) PRIMARY KEY, WHName NVARCHAR(50) NOT NULL ); CREATE TABLE Inventory ( ItemCode NVARCHAR(32) NOT NULL, WHCode NVARCHAR(20) NOT NULL, Qty DECIMAL(18,4) NOT NULL DEFAULT 0, LastUpdate DATETIME NOT NULL DEFAULT GETDATE(), CONSTRAINT PK_Inventory PRIMARY KEY (ItemCode, WHCode) ); CREATE TABLE InventoryFlow ( FlowID BIGINT IDENTITY(1,1) PRIMARY KEY, DocType NVARCHAR(10) NOT NULL, -- 采购入库、销售出库、领料、退料、盘点 DocNo NVARCHAR(30) NOT NULL, ItemCode NVARCHAR(32) NOT NULL, WHCode NVARCHAR(20) NOT NULL, QtyIn DECIMAL(18,4) NOT NULL DEFAULT 0, QtyOut DECIMAL(18,4) NOT NULL DEFAULT 0, Cost DECIMAL(18,4) NOT NULL DEFAULT 0, Operator NVARCHAR(50) NULL, CreateTime DATETIME NOT NULL DEFAULT GETDATE() );这张骨架里最关键的是InventoryFlow。不要小看它所有加减库存都必须形成流水记录否则以后对账时根本没有依据。Inventory表保存的是当前结存能用一条SQL聚合出来但为了报表性能和老业务的直观查询通常保留一张热点表。数量字段必须用DECIMAL(18,4)不要用float或double。浮点数在反复加减后会出现0.0001级别的尾差短期看不出来一个财年跑下来对不上账是必然的。另一点是Cost默认值制造业常用移动加权平均法这个字段要在采购入库时按“金额/数量”计算并写入流水。4.2 用C#写“插入流水 更新库存”的事务代码业务代码里最容易翻车的是先扣库存、后写流水中间没有任何事务保护。正确的做法是把“写流水”和“改库存”放进同一个事务里。下面这一小段展示了核心思路using (SqlConnection conn new SqlConnection(connectionString)) { conn.Open(); using (SqlTransaction tx conn.BeginTransaction()) { try { // 1. 写入库存流水 SqlCommand insertFlow new SqlCommand( INSERT INTO InventoryFlow (DocType, DocNo, ItemCode, WHCode, QtyIn, QtyOut, Cost, Operator) VALUES (DocType, DocNo, ItemCode, WHCode, QtyIn, QtyOut, Cost, Operator);, conn, tx); insertFlow.Parameters.AddWithValue(DocType, PO); insertFlow.Parameters.AddWithValue(DocNo, docNo); insertFlow.Parameters.AddWithValue(ItemCode, itemCode); insertFlow.Parameters.AddWithValue(WHCode, whCode); insertFlow.Parameters.AddWithValue(QtyIn, qty); insertFlow.Parameters.AddWithValue(QtyOut, 0); insertFlow.Parameters.AddWithValue(Cost, cost); insertFlow.Parameters.AddWithValue(Operator, operatorName); insertFlow.ExecuteNonQuery(); // 2. 更新库存表 SqlCommand updateInv new SqlCommand( UPDATE Inventory SET Qty Qty QtyIn, LastUpdate GETDATE() WHERE ItemCode ItemCode AND WHCode WHCode;, conn, tx); updateInv.Parameters.AddWithValue(QtyIn, qty); updateInv.Parameters.AddWithValue(ItemCode, itemCode); updateInv.Parameters.AddWithValue(WHCode, whCode); updateInv.ExecuteNonQuery(); tx.Commit(); } catch { tx.Rollback(); throw; } } }这段代码使用了BeginTransaction和SqlTransaction保证流水和库存要么一起成功要么一起回滚。参数化写法是为了防止SQL注入同时也是老ERP源码里很稀缺的好习惯。你需要注意的是Inventory表可能已有数据如果执行UPDATE时影响行数为0应当先插入库存初始化数据。更稳妥的写法是在更新前检查EXISTS或者用MERGE核心目的都是避免“有了流水却没有库存行”的孤儿情况。当库存可能不足时要把UPDATE Inventory SET Qty Qty - QtyOut WHERE Qty QtyOut ...放在事务里先判断受影响行数。如果为0直接抛业务异常并回滚。在并发环境下这种“条件更新”比先查后更安全得多。4.3 工单领料为什么要走“占用—实发—回冲”而不是直接扣数制造业工单领料在业务上有一个特殊需求计划员先把生产工单在系统里审核按BOM自动生成“应领数量”但仓库不一定一次性把所有材料都发到车间。常见流程是“按单占用、分批实发、超领控制、缺料回冲”。源码如果做得正规会有一个“领料占用表”把工单所需料号的数量锁起来但不直接扣减库存。真正从仓库拉料时再把占用数转成实发数同时扣减可用库存。如果车间发现工单被取消或BOM改动已占用未实发的部分要“回冲”释放回库存。这个机制能避免仓库账面数不变、可用量却超卖的情况。从实现角度看这比直接的库存扣减复杂一些但核心还是事务。在C#里占用、实发、回冲的每一笔都要写流水且流水类型区分清楚。后期对成本时只要把同一工单下所有领料流水的费用汇总再减去退料流水就可以得到工单材料成本。这也是“制造业进销存”与普通进销存在专业性上的根本差别。5. 从“能登录”到“敢上线”四个高频踩坑与排查清单5.1 登录后跳转500根因多半是Session和虚拟目录现象页面能打开输入账号密码后点击登录浏览器先是循环跳转最后显示500或404或者登录成功过一会儿又退回登录页。原因老ASP.NET应用里登录状态靠Session变量判断Session失效或无法保存就会出现“明明登录过却一直回到登录页”。另外如果站点部署在IIS虚拟目录下而重定向地址写的是绝对路径也会跳到错误地址。解决第一步确认SessionState配置。在web.config的system.web节点里找到sessionState modeInProc /InProc表示进程内SessionIIS应用池被回收就全部丢适合开发生产环境建议改StateServer或SQLServer。第二步检查IIS里站点是否为独立应用程序虚拟目录下面不能直接部署要在IIS中“转换为应用程序”。第三步检查Cookie路径如果站点路径不是根路径Cookie会默认按根级写容易互相覆盖。5.2 日期格式在制造业盘点里“隐形翻车”现象盘点单明明输入了“2024-11-04 08:30”保存后在数据库里变成“2024-04-11 08:30”或者报表日期总是偏一天。原因老源码常用Convert.ToDateTime(string)或DateTime.Parse(string)这两种方法依赖服务器区域设置。中国操作系统一般是yyyy/MM/dd或yyyy-MM-dd但某些IIS服务器的区域设置被运维改成了en-US系统就会把“04”当作月份或把“11”当作日期。解决在业务层强制指定格式例如DateTime.ParseExact(input, yyyy-MM-dd, CultureInfo.InvariantCulture)。更彻底的办法是让页面用日期控件直接提交格式化字符串数据库字段用datetime2C#模型中全部用DateTime类型强强对应。填入数据后再执行一次“SELECT MIN(CreateTime), MAX(CreateTime)”做抽查成本很低但能避免大事故。5.3 数量字段用float月底对账对不上现象系统刚开始使用时结转库存很准使用三个月到半年后某些料号的库存余量出现0.0000001级别的偏差且差异无法追溯到任何单据。原因C#里如果用double或floatSQL表字段用float加减运算会引入二进制浮点舍入误差。虽然单笔误差极小但制造业频繁出入库一年累计下来足以让财务产生怀疑。解决所有数量、金额、单价字段在SQL中统一改成DECIMAL(18,4)或更精确的DECIMAL(20,6)C#代码对应使用decimal不再使用double。这个改动不是改一两个类就完事要把所有计算汇总、报表导出里的强转一并检查。老项目若已经跑出脏数据一般做法是重算库存把所有流水按时间顺序回放重新生成库存余额表。5.4 并发领料导致库存负数不是系统慢是缺少锁现象两个仓库管理员同时对一个料号出库系统都显示扣减成功但盘点时发现总账库存变成负数或者明明库存只剩3个两个人却分别成功领了3个和1个。原因代码先SELECT Qty判断大于等于出库数再UPDATE。两个连接同时读到Qty3都认为可以出库然后各自更新最终变成负数。这属于典型的并发一致性漏洞。解决把判断和扣减合并成一条带条件的SQL。例如UPDATE Inventory SET Qty Qty - outQty WHERE ItemCode code AND WHCode wh AND Qty outQty然后检查ExecuteNonQuery()返回值影响行数为0就说明库存不足直接抛异常回滚。如果要保留“先查后更”的逻辑可以在查询时加上WITH (UPDLOCK, ROWLOCK)提示锁住该行直到事务结束。6. 老源码的续命方向先做对账脚本再做REST化改造拿到源码并顺利登录以后最容易出现的膨胀心理是“系统能跑了业务可以交出去”。我见过太多上线后拿电筒找原始Excel的情况真正应该做的第一件事不是加功能而是把账做平。这里分享一个我每次接手进销存都会立刻执行的SQL对账脚本SELECT I.ItemCode, I.WHCode, I.Qty AS InvQty, SUM(F.QtyIn - F.QtyOut) AS FlowQty, I.Qty - SUM(F.QtyIn - F.QtyOut) AS DiffQty FROM Inventory I LEFT JOIN InventoryFlow F ON I.ItemCode F.ItemCode AND I.WHCode F.WHCode GROUP BY I.ItemCode, I.WHCode, I.Qty HAVING ABS(I.Qty - SUM(F.QtyIn - F.QtyOut)) 0.0001;这个脚本的目标是找出“库存余额表”和“流水明细”不一致的所有料号。如果DiffQty不为0说明系统里有单据更新了流水但没有更新库存或者有人直接改库存表。修数据时不要手动改数字而是补红冲单、重算库存或者重新初始化余额保存操作痕迹。这一步做完系统才算真正立住。之后的改造方向我建议按“服务层优先REST化后来”推进。先在老项目里将采购、销售、领料等业务逻辑从aspx页面里抽到独立的业务类让页面只负责接收参数和展示结果。等业务类稳定后再引入ASP.NET Core作为API项目把服务层方法逐一暴露成POST接口。前端不用纠结是MVC还是Razor可以让将来的新客户端、手机端、扫码设备继续调用同一套后端逻辑。最后说一条血泪经验我参与过的项目里最贵的一次修复不是代码逻辑写错而是一个存储过程把日期字段定义为varchar导致月末成本总是偏一天。从那之后我养成了一个习惯凡是钱、量、日期都在数据库层定死类型在C#层用强类型校验绝不让字符串到处乱传。老进销存源码值不值得用主要看它是不是把这条底线守住了。守住就有救守不住再漂亮的界面也只是空壳。希望帮到你。本文还有配套的精品资源点击获取
返回列表