☰
ASP.NET进销存源码:从部署到二次开发实战指南
2026/10/7 17:04:49 网站建设 项目流程

简介:基于ASP.NET的ERP进销存管理系统完整源码,适合.NET方向初学者、课程设计或毕业设计使用,也可供需要了解传统进销存业务流程的开发人员参考。该系统围绕采购、销售、库存等核心业务设计,开发环境为Visual Studio 2010与SQL Server 2008R2,基于.NET 4.0,数据库文件置于DB_sq文件夹,附加数据库后修改web.config连接字符串即可运行,默认管理员账号与密码均为8001。资源包总大小27.33MB,共2000个文件,其中C#源代码文件460个、ASPX页面351个、JavaScript脚本802个、CSS样式120个,并包含数据库备份、项目配置、图片图表、Excel表格及公共控件类文件,基本覆盖前端交互、后台业务逻辑到数据库存储的完整链路。目前已有243人学习,通过这套源码可以梳理ASP.NET多层架构、控件封装、权限登录与库存流转等关键实现,对快速搭建演示环境和二次开发很有帮助,尤其适合做课程答辩或项目复现。

1. 一套ASP.NET进销存源码,还值不值得用,先看它能给你省什么

最近不少朋友发消息问:现在都在聊ASP.NET Core、Vue3后台管理系统,甚至SpringBoot+Vue商品管理系统,一套老ASP.NET的ERP进销存源码还有啥可看?我的答案是:如果你是要给中小工厂、商贸公司做一套能用的进销存管理系统,这种老源码反而是最快跑通的选项。它把采购、销售、库存、应收应付几件事闭环了,你拿来改二次开发,比自己从零写SaaS快太多。适合接单的工程师,也适合想弄懂真实业务流的新手。打开源码第一件事不是崇拜技术,而是看它能不能算清账。

2. 看懂ERP源码的骨架:ASP.NET老项目为什么长这样,目录、数据库、访问层怎么配合

拿到一套ERP进销存源码,别急着按F5。先打开解决方案,看文件后缀。如果是.aspx、.ascx、.ashx,基本就是ASP.NET Web Forms。很多人一看见Web Forms就皱眉,但国内还在线上跑的中小企业ERP,一大半还是Web Forms写的,甚至有些是从Delphi 7的进销存迁移过来的。这不是说Web Forms有多先进,而是它足够稳,改起来门槛低。一个页面配一个.aspx.cs,出问题打开文件就能改,不需要理解复杂的中间件管线。所以我看到这类源码的第一反应是:先把它跑通,再看它在哪些地方值得重构,而不是全盘推翻。

2.1 ASP.NET WebForms仍是中小ERP主力,Core和MVC是迁移方向

为什么Web Forms能活到今天?原因在回发模型。控件拖上去,事件写在后置代码,开发体验接近老的WinForm。对于没有专职前端的团队,后端工程师一个人能搞定录单页面、列表、权限,这是很大的优势。而ASP.NET Core MVC虽然新,但要求你理解Startup、依赖注入、Razor布局、中间件,改一个页面要动的文件更多。如果你拿到源码后想用ASP.NET Core MVC重写,等于把业务重做一遍,风险远大于收益。

我的判断是:新项目可以优先选ASP.NET Core MVC或者Vue3后台管理系统做前后端分离;手上已经能跑的ERP源码,最佳路线是保留WebForms作为录入端,在项目里加Web API接口。老ERP也可以同时提供JSON接口给手机端或新前端调用。这样既不动核心录单流程,又能让仓库扫码枪、老板手机报表这些新场景接入。

2.2 源码目录里的三层:UI层、BLL层、DAL层各管什么

拿到源码先看解决方案结构。典型的长这样:

ERP.sln ├─ Web // UI层:aspx页面 + 用户控件 │ ├─ Common/ // 公共类:Session判断、权限控件、工具函数 │ ├─ Pages/ │ │ ├─ Purchase/ // 采购:采购单、入库单、退货单 │ │ ├─ Sales/ // 销售:销售单、出库单、客户管理 │ │ ├─ Inventory/ // 库存:库存查询、调拨、盘点 │ │ └─ Finance/ // 应收应付、收款付款单 ├─ BLL // 业务逻辑:单据编号、库存校验、允许负库存判断 ├─ DAL // 数据访问:SQL语句或存储过程 ├─ Model // 实体类:对应数据库表 └─ DB // SQL脚本:建库、建表、初始数据

别指望每套都规范。很多老源码其实是“两层半”:页面里直接new SqlConnection,业务逻辑都写在.aspx.cs里。这种反而好改,因为改动集中,搜索“SqlConnection”就能定位所有数据访问点。我拿到源码会先全局搜“SELECT”和“INSERT”,看SQL是写在DAL的一个大文件里,还是散在页面。如果集中在DAL层,说明作者有分层意识;如果散在页面里,也别急,先跑通,二次开发时把要改的查询抽到DAL。

给一个DAL层典型方法,注意参数化:

public DataTable GetStockList(string keyword) { using (SqlConnection conn = new SqlConnection(connectionString)) { string sql = @" SELECT p.ProductCode, p.ProductName, ISNULL(s.Quantity,0) AS Quantity FROM Product p LEFT JOIN Stock s ON p.ProductId = s.ProductId WHERE p.ProductCode LIKE @kw OR p.ProductName LIKE @kw ORDER BY p.ProductCode"; using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@kw", "%" + keyword + "%"); SqlDataAdapter da = new SqlDataAdapter(cmd); DataTable dt = new DataTable(); da.Fill(dt); return dt; } } }

这里用LEFT JOIN是因为有些商品还没有建立库存记录,如果改用INNER JOIN,这类商品在列表里就消失了,业务上会以为漏数据。参数化是必须的,ERP源码里被SQL注入了,整张商品表都可能被删。老代码里常见“string sql = "select * from Product where Name='" + txtName.Text + "'"”,这种写法必须改,没有商量。

再补一段:BLL层的事务边界。看源码时搜“BeginTransaction”,如果只有新增订单时用,说明作者把多表写操作当成一个原子操作。这样库存流水和单据才不会一半成功一半失败。有些源码连事务都没用,入库单主表插进去了,明细插到一半报错,库存却已经加上,月底对账就全靠手工。这种源码拿来练手可以,直接上生产要慎重。

2.3 数据库脚本:进销存核心表的主外键关系

进销存系统的数据库不会很复杂,但表与表之间的关系必须看明白。常见核心表如下:

表名用途关键字段
Product商品ProductId, ProductCode, ProductName, 单位, 库存上下限
Stock当前库存ProductId, WarehouseId, Quantity
StockLog库存流水LogId, BillType, BillNo, ProductId, ChangeQty, CreateTime
PurchaseOrder采购单主表OrderId, SupplierId, TotalAmount, Status, CreateTime
PurchaseOrderDetail采购单明细OrderId, ProductId, Quantity, Price, Amount
SaleOrder销售主表OrderId, CustomerId, TotalAmount, Status, CreateTime
SaleOrderDetail销售明细OrderId, ProductId, Quantity, Price, Amount
ARAP应收应付客户/供应商, 方向, 单据号, Amount, Balance

这里最关键的不是主外键约束,而是StockLog这张流水表。只要每次入库出库都写流水,月底库存对不上时还能按流水追到具体单据。很多源码为了省事,只更新Stock表里的Quantity,不写流水,或者流水里不带单据号,一出问题连翻盘的机会都没有。所以看源码时要搜“INSERT INTO StockLog”,搜不到就要警惕,这就是我常说的“后悔药”。

继续给出一个联表流水查询:

SELECT l.LogId, l.BillType, l.BillNo, p.ProductName, l.ChangeQty, u.UserName, l.CreateTime FROM StockLog l LEFT JOIN Product p ON l.ProductId = p.ProductId LEFT JOIN SysUser u ON l.CreateBy = u.UserId WHERE l.ProductId = @ProductId ORDER BY l.CreateTime DESC;

StockLog表必须建索引,至少要有(ProductId, CreateTime)。没有索引时,库存详情页一查就是全表扫描,数据量到10万行就开始“转圈”。我在接一个老项目时就从日志里看到这种慢查询,加完索引查询从8秒掉到300毫秒。

还有一个细节:现代进销存会在库存表里加LockQty(锁定数量)。销售订单已审核未发货时,把数量记到LockQty,发完货再减。这样才能避免“订单占了货,别人还能卖”的情况。老源码如果只有Quantity,那就要在业务上规定“审核即扣减”,但这样订单取消时又要把库存加回来,逻辑绕来绕去。能看得懂这个区别,就算是把骨架看懂了。

3. 把源码跑起来:本地IIS部署、数据库还原和连接字符串的坑

拿到源码一定先在自己电脑上跑通,再往客户服务器上放。这套ASP.NET ERP对运行环境有要求,最好在Windows上用Visual Studio + SQL Server。下面是我验证源码时走的完整流程。

3.1 环境准备:Visual Studio版本、SQL Server和IIS组件

先看Web项目文件里的TargetFramework。如果是.NET Framework 4.x,用Visual Studio 2019或2022都能处理;如果是3.5版本,用VS2019打开后建议先不要盲目升级,直接右键项目选“属性”把目标框架改成4.7.2,然后编译试试。老源码如果用到了第三方控件,升级目标框架可能触发授权问题,所以先保持原样,能跑通再动。

SQL Server用2019或2022都行,Express版也能跑,只是有数据库大小限制。还原数据库时注意:源码里数据库名可能是ERP,但你一定要还原成Initial Catalog对应的名字。如果连接字符串写的是“database=ERP”,你还原成ERP_test,不改连接字符串永远连不上。

IIS组件这一项最容易翻车。Windows功能里要勾选“.NET Framework 3.5/4.8 功能”和“IIS的ASP.NET”模块,否则发布到IIS后浏览器直接500.19或者“处理程序 PageHandlerFactory 未找到”。我一直用命令行一次性装好:

dism /online /enable-feature /featurename:NetFx3 /featurename:NetFx4-AdvAspNet dism /online /enable-feature /featurename:IIS-WebServerRole /featurename:IIS-ASPNET

注意:NetFx3是3.5运行时,老ERP经常需要;NetFx4-AdvAspNet是ASP.NET 4.x运行。装完IIS后记得重启站点。这个报错非常玄学,其实环境没装对。

3.2 还原数据库并改连接字符串:搜索“Data Source”是第一步

打开SQL Server Management Studio,右键“数据库”→“还原”。然后打开Web.config,找到connectionStrings节点。常见写法:

<connectionStrings> <add name="ERPConnection" connectionString="data source=.;initial catalog=ERP;user id=sa;password=你的密码;MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>

参数说明:data source=localhost或者服务器IP;initial catalog是数据库名;user id/password是SQL登录账号,不要用“Integrated Security=True”发布到服务器时还要改回本地。MultipleActiveResultSets建议保留,老代码经常一次性开多个DataReader。

重点来了:连接字符串很可能不止在Web.config里。有的是在DAL层静态类里写死“server=192.168.1.100”。所以必须全局搜索“Data Source”或“server=”,把所有漏网之鱼找出来。我接手一个项目时改完Web.config还是连不上,最后发现Model层有个ConnectionHelper.cs,里面又写了一份。

如果连接字符串被加密了怎么办?常见做法是在WebConfig里看到EncryptedData,这时用aspnet_regiis -pd解密,或者找源码里是否有解密函数。有些源码作者会保留明文,只是加了注释。不要自己猜,搜索“Decrypt”更快。

3.3 编译与部署:直接运行和发布到IIS是两套逻辑

在Visual Studio里按F5会用IIS Express跑起来,这是最快的验证方式。如果项目引用了一些老包,先右键解决方案“还原NuGet包”。有些源码带的packages文件夹在压缩包里,解压后不还原也能编译。

发布到IIS时,我一般右键Web项目,“发布”→“文件夹”,目标路径指到服务器网站根目录。然后在IIS里创建应用程序池,选择.NET Framework v4.0经典或集成。这里有个老坑:如果源码用了“集成管线”下无法识别的处理程序,就切到“经典”模式。反过来,如果用了ASP.NET Routing,必须用集成模式。所以部署后先看一眼网站是否能打开“Login.aspx”,如果500,再切换模式。

还要给网站目录写权限。老ERP经常要写日志、生成Excel、上传商品图片,默认的IIS_IUSERS只给读,造成保存单据报错。解决办法是给网站的“写入”权限,同时把图片上传目录排除在写权限外会更安全。

如果只是内部测试,也可以把VS发布出来的文件夹复制到服务器,然后创建一个“应用程序”指向这个文件夹。注意不是把源码放上去,而是发布后的文件加上bin目录。整套流程跑通后,建议把网站日志打开,排查500时看Windows事件查看器里ASP.NET 2.0/4.0的异常。

为了减少“黑匣子”问题,我把web.config里的customErrors mode改为Off:

<system.web> <customErrors mode="Off"/> <compilation debug="true" targetFramework="4.7.2"/> </system.web>

这样页面报错会显示明细行。生产环境记得改回On,不然错误堆栈会暴露SQL语句。

3.4 部署后先调的三个配置项:Session超时、上传大小、连接池

老ERP默认Session 20分钟,用户中午休息一会儿回来就掉线。在web.config:

<system.web> <sessionState mode="InProc" timeout="60" cookieless="false" /> </system.web>

说明InProc是最常见模式,但应用池回收会丢Session。如果你站点部署在单机,调大timeout即可;如果在多台服务器,必须改成StateServer或SQLServer。判断依据是看站点是单机还是负载均衡。

第二个是上传大小。商品图片经常超过浏览器默认的4MB限制。在system.webServer里:

<security> <requestFiltering> <requestLimits maxAllowedContentLength="104857600" /> </requestFiltering> </security>

同时给页面上的FileUpload设置maxRequestLength。第三个是数据库连接池。老源码频繁new SqlConnection,关闭不及时会导致连接池耗尽。最稳妥的做法是用using包裹连接,并设置连接字符串的Pooling=True,默认就是。如果连接池爆了,看报错“Timeout expired”,多半是有的连接没Close。

4. 进销存主流程的代码实现:采购入库、销售出库和库存扣减的闭环

进销存源码的核心不是界面,是几个“写操作”的代码。把采购入库、销售出库、库存盘点这几个事务读透,这套系统你就能改。

4.1 采购入库单提交时,代码如何同时写主表、明细和库存流水

老ERP保存采购入库单,最少要动四张表:采购主表、采购明细、库存表、库存流水。看下面这段典型事务代码:

using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlTransaction tran = conn.BeginTransaction(); try { // 1. 插入采购入库主表,拿到自增单号 string sqlMaster = @"INSERT INTO PurchaseOrder(SupplierId, TotalAmount, Status, CreateBy, CreateTime) VALUES(@SupplierId, @TotalAmount, '已审核', @CreateBy, GETDATE()); SELECT SCOPE_IDENTITY();"; SqlCommand cmdMaster = new SqlCommand(sqlMaster, conn, tran); cmdMaster.Parameters.AddWithValue("@SupplierId", supplierId); cmdMaster.Parameters.AddWithValue("@TotalAmount", totalAmount); cmdMaster.Parameters.AddWithValue("@CreateBy", userId); int orderId = Convert.ToInt32(cmdMaster.ExecuteScalar()); string billNo = "PO" + orderId.ToString().PadLeft(6, '0'); // 2. 循环写明细 + 库存 + 流水 foreach (var item in details) { SqlCommand cmd = new SqlCommand(@" INSERT INTO PurchaseOrderDetail(OrderId, ProductId, Quantity, Price, Amount) VALUES(@OrderId, @ProductId, @Quantity, @Price, @Amount); IF EXISTS(SELECT 1 FROM Stock WHERE ProductId=@ProductId AND WarehouseId=@WarehouseId) UPDATE Stock SET Quantity=Quantity+@Quantity WHERE ProductId=@ProductId AND WarehouseId=@WarehouseId; ELSE INSERT INTO Stock(ProductId, WarehouseId, Quantity) VALUES(@ProductId, @WarehouseId, @Quantity); INSERT INTO StockLog(BillType, BillNo, ProductId, ChangeQty, CreateTime) VALUES('采购入库', @BillNo, @ProductId, @Quantity, GETDATE());", conn, tran); cmd.Parameters.AddWithValue("@OrderId", orderId); cmd.Parameters.AddWithValue("@ProductId", item.ProductId); cmd.Parameters.AddWithValue("@Quantity", item.Quantity); cmd.Parameters.AddWithValue("@Price", item.Price); cmd.Parameters.AddWithValue("@Amount", item.Quantity * item.Price); cmd.Parameters.AddWithValue("@WarehouseId", warehouseId); cmd.Parameters.AddWithValue("@BillNo", billNo); cmd.ExecuteNonQuery(); } tran.Commit(); } catch { tran.Rollback(); throw; } }

逻辑说明:这个方法是典型的“一个事务管四张表”。注意我故意把单据编号用自增ID拼出来,为的是让你看清楚编号是怎么生成的。真实项目里单据号一般单独建BillNo表,用存储过程批量生成,避免并发拿重号。所以你在源码里看到“SELECT MAX(BillNo)”这种写法,要小心:多用户同时点保存,可能拿到同一个单号。解决方式是建唯一索引,或者改成“独立编号表 + 行锁”。这一步很关键。

参数说明:加参数用AddWithValue是偷懒,正式代码最好用SqlDbType明确类型。AddWithValue在SQL Server上对NVarChar和DateTime有时会造成隐式转换,导致索引失效。这也是老源码性能差的一个隐藏点。

4.2 销售出库扣库存:先查后扣为什么会超卖,锁和事务怎么写

很多简单系统用的是“先查库存再扣”的写法,看起来没毛病,一上多人就出事:

// 反例:先查再扣,两个请求同时读到库存=10都会通过,最后超卖 int stock = GetStock(productId); if (stock >= qty) { UpdateStock(productId, stock - qty); }

原因在于两个销售员同时录单,都读到可用库存10,A扣了8,B也扣了8,账面就变成-6。这是进销存并发里最经典的“超卖”问题。解决的办法是直接把库存条件写进UPDATE,用受影响行数判断:

string sql = @"UPDATE Stock SET Quantity = Quantity - @Qty WHERE ProductId = @ProductId AND Quantity >= @Qty; IF @@ROWCOUNT = 0 THROW 50001, '库存不足', 1;"; SqlCommand cmd = new SqlCommand(sql, conn); cmd.Parameters.AddWithValue("@Qty", qty); cmd.Parameters.AddWithValue("@ProductId", productId); cmd.ExecuteNonQuery();

参数里的@Qty是本次出库数量,@ProductId是商品ID。重点是“Quantity >= @Qty”这个条件,它让数据库在扣减的同时完成校验,一个语句就是原子操作。如果受影响行数为0,就抛错回滚,这样不会出现负数库存。

如果用的SQL Server版本较老,不支持THROW,可以用RAISERROR。有些源码还会要求负库存不允许过账,那就在UPDATE前再加一个锁查询(SELECT ... WITH(UPDLOCK))。但最稳妥的还是UPDATE条件判断,因为它不需要额外锁。

还要注意:扣完库存后,必须写一条“销售出库”流水,并且关联销售单号。如果客户退货,就写一条负数的出库流水或者“销售退货”类型,让对账时有来有回。很多源码直接“删除出库记录”来冲正,这是错误的做法,审计时会死。

4.3 应收应付与库存对账:月底不平的常见原因

库存和财务对不上,通常不是数量记错,是成本算法不一致。老ERP很多用“最近进价”给出库成本,这在新旧采购单价相差很大时,月底库存金额全是乱的。正确做法是移动加权平均:

// 新成本 = (原库存金额 + 本次入库金额) / (原库存数量 + 本次入库数量) decimal newCost = (oldQty * oldCost + buyQty * buyPrice) / (oldQty + buyQty);

这段计算必须放在入库事务里,不能放到page里。如果两个采购单同时入库同一商品,两个请求都读到旧成本,计算出的新成本就会互相覆盖。所以成本计算和入库单要同一个事务,且对商品行加锁。

应收应付不平,还有一大半原因是核销没做干净。老源码经常是客户付款后直接更新ARAP表的Balance字段,没有记录“这张收款单核销了哪几张销售单”。月底对账时财务只能看到余额,解释不清。我建议二次开发时至少加一张“核销明细表”,记录收款单号、来源单号、核销金额。ARAP表只留当前余额,所有变动走流水。这样即使有一笔付款对错了单子,也能在流水里翻出来重挂,算是给自己留一张后悔药。

4.4 盘点单的代码写法:盘盈盘亏不是直接改库存数字

盘点时账面数是Stock.Quantity,实际数来自Excel或扫码枪,差异要生成一张盘点单,然后通过StockLog写“盘盈/盘亏”流水。禁止直接UPDATE Stock,必须留痕迹。给一个SQL:

INSERT INTO StockLog(BillType, BillNo, ProductId, ChangeQty, Remark) VALUES('盘点调整', @CheckNo, @ProductId, @DiffQty, @Remark); UPDATE Stock SET Quantity = Quantity + @DiffQty WHERE ProductId = @ProductId;

说明:@DiffQty可正可负,但要与盘点单明细一一对应。老源码里很多没有盘点流程,只有“调整库存”按钮。这种要小心审计。

5. 改这套ERP源码最容易踩的5个坑:从登录超时到页面乱码

改老ERP源码,比写新系统更容易翻车,因为代码里藏着很多隐性约定。下面五个坑我基本都踩过,按现象、原因、解决写清楚。

5.1 改了页面后所有操作跳回登录页:Session失效和IIS应用池回收

现象:登录进去,点保存单据,页面直接跳回Login.aspx。反复登录也没用。

原因:老ERP用Session存登录状态,默认是InProc(进程内)。IIS应用池有个“闲置超时”,默认20分钟,一旦回收,进程里的Session全部清空。另外,Web.config里如果sessionState timeout是20,用户挂机超时也会掉线。还有一种情况:代码里在Application_Start中预加载了数据字典,只要这个事件异常,所有Session也变相失效。

解决:把Session超时调到60或120分钟,并让应用池“闲置超时”设为0(即不超时回收)。如果有多台服务器,必须改SessionState为StateServer或SQLServer,否则单机这招只能稳住一台。再有,写一个全局的LoginBasePage,在Page_Load里判断Session["User"],不要在每一个页面复制粘贴判断逻辑。

5.2 日期查询总差一天:闭区间查询和时区偏移

现象:用户选“2025-06-10”,当天的订单查不出来,只有9号及之前的。

原因:查不到当天,是因为老代码把TextBox的文本当成“2025-06-10 00:00:00”,数据库里的CreateTime是“2025-06-10 15:30:00”,用“CreateTime <= '2025-06-10'”就把当天所有时间给排除掉了。还有另一个时区坑:浏览器JavaScript从Date对象序列化时,会按UTC生成“2025-06-09T16:00:00Z”,某一些老控件直接绑定隐藏字段,导致服务器拿到的是前一天。

解决:统一在服务端解析日期,并且查询条件改成半开区间“CreateTime >= @start AND CreateTime < @end”,其中@end传入“结束日期 + 1天”。不要用LIKE来匹配日期,也不要对CreateTime做CONVERT截断,否则索引失效。

5.3 列表超5万行就卡:GridView自带分页是假分页

现象:库存流水打开10万行,页面等了十秒,翻下一页又等十秒。

原因:GridView搭配SqlDataSource自带AllowPaging,默认分页是先不分页地把所有行Select出来,再在控件层跳过。数据量一大,数据库、带宽、页面内存全部被拖垮。很多老ERP都有这种列表。

解决:改成服务端分页。SQL Server 2012以上可用OFFSET/FETCH:

SELECT * FROM StockLog ORDER BY LogId DESC OFFSET @PageSize * (@PageIndex - 1) ROWS FETCH NEXT @PageSize ROWS ONLY;

或者用ROW_NUMBER(),然后做两个连接:一个查总数,一个查当前页。我一般会把分页参数放存储过程,避免把动态SQL拼到前端。改完这一步,10万行列表也能秒开。

5.4 新菜单分配了权限但用户看不到:缓存失效和菜单表条件

现象:管理员给角色新增了一个菜单,重新登录还是看不到。

原因:老系统为了省去每次请求都查数据库,在Application或Session里缓存了菜单树。角色权限变更后没有清缓存。还有一种很隐蔽的是菜单表里的菜单项IsDeleted=0或IsVisible=1,新增时默认值不对。

解决:找到缓存键,一般搜索“MenuTree”或“Permission.GetMenu”。修改角色权限时调用Cache.Remove。如果怎么都找不到,最简单的办法是重启IIS应用池,先确认是不是缓存问题。但真正根除还是给权限表加Version,在菜单树缓存键里带上版本号,授权时Version自增,新请求拿到的就是新菜单。做二次开发时,我会优先用Session存userId和roleId,菜单动态回读,不做缓存。

5.5 导出Excel中文乱码:BOM头和Response.ContentEncoding

现象:页面生成CSV,用Excel打开中文全是“锟斤拷”。

原因:Response.ContentEncoding没设UTF-8,或者设了但没有写BOM。老浏览器和Excel对无BOM的UTF-8识别成ANSI。

解决:在输出前加BOM头:

Response.Clear(); Response.ContentEncoding = Encoding.UTF8; Response.ContentType = "text/csv; charset=utf-8"; Response.Write("\uFEFF"); // 然后写数据 Response.End();

注意不要先Response.End再写。如果文件名里有中文,还要记得用HttpUtility.UrlEncode,否则另存为时文件名乱码。这个办法对CSV和XML通用。以上五条,前三条大多是环境问题,后两条是代码问题。遇到时先开customErrors看异常,再按这个顺序检查。

6. 二次开发最值得先做的三件事:Web API移动端、报表提速和多单位换算

这三个方向不改变原有录单逻辑,但能让老ERP源码在2025年继续用下去。

6.1 给老ERP加登录接口:用一般处理程序返回JSON

想接入扫码枪或Vue3后台管理系统,最省事的是在Web项目里新增一个handler.ashx,在ProcessRequest里写登录校验,返回JSON:

public void ProcessRequest(HttpContext context) { string user = context.Request.Form["username"]; string pwd = context.Request.Form["password"]; // 去用户表校验 bool ok = new UserBll().CheckLogin(user, pwd); var result = new { ok = ok, token = Guid.NewGuid().ToString("N") }; context.Response.ContentType = "application/json"; context.Response.Write(JsonConvert.SerializeObject(result)); }

说明:POST请求,密码建议先用MD5/SHA256加盐再比对,不要明文。这个ashx不需要Session,移动端每次请求带token。

6.2 把库存汇总报表写成UNION ALL查询,从分钟级降到秒级

很多老ERP的销售报表是“先查明细,在页面循环汇总”,数据一多就卡。改成直接在数据库按天汇总:

SELECT CONVERT(varchar, CreateTime, 112) AS BillDate, COUNT(*) AS BillCount, SUM(TotalAmount) AS TotalAmount FROM SaleOrder WHERE CreateTime >= @start AND CreateTime < @end GROUP BY CONVERT(varchar, CreateTime, 112) ORDER BY BillDate;

说明:这是典型的按时间分区汇总。一定要用CreateTime < @end,不是<=,避免重复统计。如果数据量继续涨,再加一个“每日汇总表”定时刷新,应用层只读汇总表。

6.3 多单位换算:不要改主表字段,加一张换算表

商品存在箱、瓶、件等不同单位,直接在Product表加“单位换算率”字段的做法,遇到一商品多单位就乱了。建议加独立换算表:

字段说明
ProductId商品
FromUnit原单位,如“箱”
ToUnit目标单位,如“瓶”
Factor1箱=12瓶,Factor=12

单据里记录数量+单位,保存时通过Factor换算成基础单位(瓶)去扣库存。这样报表显示可以灵活换单位,库存永不以“箱”为唯一标准。

这三件事做完,老源码就不再是一个焊死的黑匣子,而是能接手机、能快速出报表、能应对多单位业务的底座。我自己吃过最大的亏就是改代码前没先备份数据库,改完成本算法后库存对不上,加了三天班。从那以后,“先备份、后动手、最后对流水”成了铁律。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询