简介:基于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 | 目标单位,如“瓶” |
| Factor | 1箱=12瓶,Factor=12 |
单据里记录数量+单位,保存时通过Factor换算成基础单位(瓶)去扣库存。这样报表显示可以灵活换单位,库存永不以“箱”为唯一标准。
这三件事做完,老源码就不再是一个焊死的黑匣子,而是能接手机、能快速出报表、能应对多单位业务的底座。我自己吃过最大的亏就是改代码前没先备份数据库,改完成本算法后库存对不上,加了三天班。从那以后,“先备份、后动手、最后对流水”成了铁律。希望帮到你。
本文还有配套的精品资源,点击获取