ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ASP.NET进销存源码实战:架构、事务与库存管理

ASP.NET进销存源码实战:架构、事务与库存管理 简介面向需要学习或二次开发进销存系统的ASP.NET开发者这份源码以VS2008SQL2005为运行环境借助DXperience控件搭建界面完整覆盖业务管理、报表管理、基础数据、系统管理、文件管理及设备管理等模块从采购、生产到销售形成较完整的企业业务闭环。压缩包约22.98MB共1119个文件以538个cs核心源码为主辅以resx/resources界面资源、45个dll依赖库及scc版本控制文件目录结构清晰便于按模块定位和查阅。已有462人浏览学习适合具备一定ASP.NET基础、希望掌握企业级业务流编码实践的开发者参考。源码从客户订单、物料需求计划到采购入库、生产领料、委外加工、销售出库等常见业务单据再到退料、报废、调拨等多份统计与明细报表可帮助深入理解进销存全链路的数据流转BOM、物料编码、操作员授权等基础设置以及文件管理、设备管理相关单据也为后续扩展和二次开发提供完整抓手。1. 先别急着下载这套 ASP.NET 进销存源码到底能给你什么ASP.NET 进销存管理系统源码听起来是个有点“年代感”的词但恰恰是这类项目在过去十几年里养活了大量中小企业的信息部门和外包团队。它的核心价值不是界面多好看而是把“采购→入库→销售→出库→盘点→对账”这条业务闭环完整落到了数据库和页面里你拿到的是一套能直接看、直接改、直接跑的业务骨架。这套源码最适合三类人一是公司里的应用开发或 IT 负责人想用现成代码快速搭内部管理系统摆脱手工 Excel 对账二是刚工作一两年的开发者想找一份带三层架构、单据流程、权限和库存扣减逻辑的实战项目来练手三是技术评估者想算清楚“拿源码二次开发”和“买商业 ERP”到底哪个划算。需要先说明白进销存不等于 ERP它只负责进、销、存三条线和往来单位、操作员权限不管生产排程、财务总账。带着这个预期去选型后面就不会踩“源码缺这个缺那个”的心理落差。2. 选型先看骨架WebForms 还是 MVC项目结构怎么搭2.1 为什么市面上大量进销存源码都是 WebForms如果你去代码托管平台或老外包公司手里翻源码会发现这类项目里十套有八套是 ASP.NET WebForms后缀是.aspx加.aspx.cs。这不是巧合而是时代选择2005 到 2012 年前后.NET Framework 2.0 到 4.5 是 Windows 服务器上的主流企业内部系统标准配置就是“Windows Server IIS SQL Server”而 WebForms 的拖控件式开发、GridView 加 SqlDataSource能让一个外包开发在两周内把增删改查页面全部堆出来。老板看到的是“今天下单明天就有系统用”程序员看到的是“不用写一堆 JavaScript 也能出页面”。放到今天如果项目是从零开始我一般不建议再开一个 WebForms 新坑ASP.NET Core MVC 或 Razor Pages 明显更适合新团队。但如果你手里已经有一套运行稳定、业务验证过的 WebForms 源码先别急着推翻重写。内部系统用户量通常只有几十人每天业务单据几百张WebForms 完全扛得住重写反而容易把已经跑通的业务流程改出问题。我的原则是以业务风险为标准选型不以技术新鲜度为标准。2.2 三层架构摆在你面前Model / DAL / BLL / Web 怎么分工拿到源码第一步不是双击.sln然后按 F5而是先看项目结构。靠谱的进销存源码一般是一个解决方案下挂四个项目名字可能略有差异但职责一致ZJX.Model实体类对应数据库表比如 Product、StockIn、StockInDetail。ZJX.DAL数据访问层封装 SqlConnection 和 SqlCommand对外提供按主键查、按条件查、插入、更新方法。ZJX.BLL业务逻辑层校验库存够不够、单据状态能不能改、事务从哪开始到哪结束。ZJX.WebWebForms 页面、用户控件、Handler。很多简化版源码会把 DAL 和 BLL 混在一个App_Code文件夹里那也不是不能用但你要意识到后期改起来会比较痛苦尤其是写单元测试的时候没有清晰的项目边界。一个典型的实体类和 SqlHelper 封装长这样public class Product { public int ProductId { get; set; } public string ProductCode { get; set; } public string ProductName { get; set; } public string Spec { get; set; } public string Unit { get; set; } public decimal CostPrice { get; set; } public decimal SalePrice { get; set; } public decimal StockQty { get; set; } public decimal MinStock { get; set; } }public static class SqlHelper { private static readonly string ConnStr ConfigurationManager.ConnectionStrings[ZjxDb].ConnectionString; public static DataTable ExecuteQuery(string sql, params SqlParameter[] parameters) { using (var conn new SqlConnection(ConnStr)) using (var cmd new SqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); var adapter new SqlDataAdapter(cmd); var dt new DataTable(); adapter.Fill(dt); return dt; } } public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (var conn new SqlConnection(ConnStr)) using (var cmd new SqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } } }这里的using不是只用来释放资源更重要的是数据库连接到底开没开、关没关。进销存系统最怕的就是连接泄漏表现为跑几天后 IIS 报“连接池已满”。另外注意参数化查询进销存系统里商品名、供应商名都是用户输入的直接拼接 SQL 不仅会被注入还会在商品名带单引号时直接让页面炸掉。参数名要和 SQL 里的ProductName一一对应写错一个运行时抛的是“必须声明标量变量”那个时刻你会很怀念仔细核对参数名的十分钟。2.3 第一步配置连接串、SQL Server 认证与常见失效原因打开源码后第一个要改的文件是Web.config。连接字符串通常在connectionStrings节点里常见写法如下connectionStrings add nameZjxDb connectionStringData Source.;Initial CatalogZjxDB;User IDsa;Passwordyour_password;MultipleActiveResultSetsTrue providerNameSystem.Data.SqlClient / /connectionStringsData Source.表示本机默认实例服务器上通常是192.168.1.10或带实例名的192.168.1.10\SQLEXPRESSInitial Catalog是数据库名要和建库脚本里的库名保持一致User IDsa是历史遗留最常见配置生产环境强烈建议改成 Windows 身份验证连接串换成Data Source.;Initial CatalogZjxDB;Integrated SecurityTrue省去管理密码的麻烦。连接串最常见的翻车点是这三处第一键名写错页面报“在配置中找不到指定连接字符串”代码里读的是ZjxDb配置文件里写的是ZjxDB或zjxdb大小写一不一致无所谓但名字必须严格匹配第二发布时Web.config被转换覆盖开发环境能连测试服务器连不上先看服务器上真实生效的配置而不是本地文件第三sa密码里带特殊字符比如分号整个连接串解析会中断。遇到连接问题不要玄学式地怀疑人生按“配置文件路径 → 键名 → 数据库实例名 → 账号密码 → 防火墙 1433 端口”逐个查五分钟定位。3. 把进销存抽象成数据模型不超十张表的表结构与索引设计3.1 核心表商品、往来单位、单据主表和明细表进销存系统的数据模型可以浓缩成一句话围绕“单据”流转。采购员录一张入库单单据头记录供应商、日期、经手人单据体记录每一件商品的数量和单价销售出库同理。所以核心表不会超过十张表名角色关键字段Product商品档案商品编码、名称、规格、单位、成本价、销售价、库存量、最低库存Supplier供应商名称、联系人、电话Customer客户名称、联系人、电话StockIn入库单主表单号、供应商、经办人、入库日期、备注StockInDetail入库单明细入库单ID、商品ID、数量、单价StockOut出库单主表单号、客户、经办人、出库日期、备注StockOutDetail出库单明细出库单ID、商品ID、数量、单价StockLog库存日志商品ID、变动类型、变动数量、变动前数量、变动后数量、关联单号、操作人、时间SysUser操作员用户名、密码哈希、角色注意主表和明细表必须成对出现这是进销存和普通增删改查的最大区别。一张入库单对应多行商品明细如果只建一张“入库记录表”把商品名塞在一个字段里用逗号拼接那后面做库存统计、按商品查流水时你会被自己的设计折磨到想重构。做数据模型时记住一句话一条业务单据 主表一条 明细多行所有流程都围绕这个结构展开。3.2 为什么必须单独建一张库存日志表新手拿到源码时最容易忽略的表是StockLog。只看业务表面库存就是商品表里的一个StockQty字段入库加、出库减完成。但只维护一个数字月底盘库对不上账时你完全没有追溯手段——账面少了十件你只知道“少了”不知道是哪张单子扣的、谁扣的、什么时候扣的。库存日志表解决的就是这个“后悔药”问题。每次库存变动不管入库、出库还是盘点调整都在同一个事务里往StockLog写一条记录记录变动前数量、变动数量、变动后数量、关联单号和操作人。这样任何时候都能回答“这个商品这一百件是怎么来的”。我见过大量企业从 Excel 迁移到进销存后第一年盘库差异很大根因就是旧系统的库存更新逻辑没写日志差异发生后只能靠翻纸质单据效率极低。所以拿到源码先检查有没有StockLog表有没有在业务代码里真正写入。没有的话这不是“简化版”是“会埋雷版”。3.3 一套可以直接改的建表 SQL 与索引建议下面这套 SQL Server 建表脚本覆盖了上述核心表字段命名做了简化方便你对照源码调整CREATE TABLE dbo.Product ( ProductId INT IDENTITY(1,1) PRIMARY KEY, ProductCode NVARCHAR(32) NOT NULL, ProductName NVARCHAR(64) NOT NULL, Spec NVARCHAR(64) NULL, Unit NVARCHAR(16) NULL, CostPrice DECIMAL(18,2) NOT NULL DEFAULT 0, SalePrice DECIMAL(18,2) NOT NULL DEFAULT 0, StockQty DECIMAL(18,2) NOT NULL DEFAULT 0, MinStock DECIMAL(18,2) NOT NULL DEFAULT 0, IsDelete BIT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX UX_Product_Code ON dbo.Product(ProductCode); CREATE TABLE dbo.StockIn ( StockInId INT IDENTITY(1,1) PRIMARY KEY, OrderNo NVARCHAR(32) NOT NULL, SupplierId INT NOT NULL, OperatorUserId INT NULL, InDate DATETIME NOT NULL DEFAULT GETDATE(), Remark NVARCHAR(255) NULL ); CREATE UNIQUE INDEX UX_StockIn_OrderNo ON dbo.StockIn(OrderNo); CREATE TABLE dbo.StockInDetail ( DetailId INT IDENTITY(1,1) PRIMARY KEY, StockInId INT NOT NULL, ProductId INT NOT NULL, Qty DECIMAL(18,2) NOT NULL, Price DECIMAL(18,2) NOT NULL ); CREATE TABLE dbo.StockLog ( LogId BIGINT IDENTITY(1,1) PRIMARY KEY, ProductId INT NOT NULL, ChangeType TINYINT NOT NULL, -- 1入库 2出库 3盘点调整 RelatedNo NVARCHAR(32) NULL, -- 关联单号 BeforeQty DECIMAL(18,2) NOT NULL, ChangeQty DECIMAL(18,2) NOT NULL, AfterQty DECIMAL(18,2) NOT NULL, OperatorUserId INT NULL, CreateTime DATETIME NOT NULL DEFAULT GETDATE() ); CREATE INDEX IX_StockLog_ProductId ON dbo.StockLog(ProductId, CreateTime DESC);商品编码要加唯一索引单据编号同理这两个字段是业务上人工可读的“身份证”不能重复。StockQty用了DECIMAL(18,2)而不是INT因为很多商品的计量单位是公斤、米、升按整数设计会在录单时被逼着四舍五入。库存日志的索引建在ProductId CreateTime DESC上是为了商品明细页里“查这个商品最近三个月的流水”能走索引否则流水表到几十万行时页面会明显变慢。4. 核心流程实现采购入库、销售出库与库存扣减4.1 采购入库主表 明细 库存更新拧进一个事务采购入库是最容易写错的核心流程。不少简化版源码的做法是先插入主表再循环插入明细然后每行明细再去 update 一次商品库存。如果这几步之间没有事务一旦第 5 行明细插入失败前面 4 行已经写进去了库存也加了一半月底对账就永远差这一截。正确做法是用SqlTransaction把这四件事包起来public void CreateStockIn(StockInEntity main, ListStockInDetailEntity details) { string connStr ConfigurationManager.ConnectionStrings[ZjxDb].ConnectionString; using (var conn new SqlConnection(connStr)) { conn.Open(); using (var tx conn.BeginTransaction()) { try { // 第一步插入入库单主表拿到自增主键 string sqlMain INSERT INTO StockIn(OrderNo, SupplierId, OperatorUserId, Remark) VALUES(OrderNo, SupplierId, OperatorUserId, Remark); SELECT SCOPE_IDENTITY();; using (var cmdMain new SqlCommand(sqlMain, conn, tx)) { cmdMain.Parameters.AddWithValue(OrderNo, main.OrderNo); cmdMain.Parameters.AddWithValue(SupplierId, main.SupplierId); cmdMain.Parameters.AddWithValue(OperatorUserId, main.OperatorUserId); cmdMain.Parameters.AddWithValue(Remark, (object)main.Remark ?? DBNull.Value); int stockInId Convert.ToInt32(cmdMain.ExecuteScalar()); // 第二步循环插入明细并同步更新库存和写日志 foreach (var d in details) { string sqlDetail INSERT INTO StockInDetail(StockInId, ProductId, Qty, Price) VALUES(StockInId, ProductId, Qty, Price);; using (var cmdDetail new SqlCommand(sqlDetail, conn, tx)) { cmdDetail.Parameters.AddWithValue(StockInId, stockInId); cmdDetail.Parameters.AddWithValue(ProductId, d.ProductId); cmdDetail.Parameters.AddWithValue(Qty, d.Qty); cmdDetail.Parameters.AddWithValue(Price, d.Price); cmdDetail.ExecuteNonQuery(); } UpdateStockAndLog(conn, tx, d.ProductId, 1, d.Qty, main.OrderNo, main.OperatorUserId); } } tx.Commit(); } catch { tx.Rollback(); throw; } } } }这段代码的关键点有三个。BeginTransaction()之后的new SqlCommand(sql, conn, tx)必须把事务对象传进去漏传事务的命令默认在另一个隐式事务里执行外层Rollback根本管不住它。SCOPE_IDENTITY()用于取当前连接和事务范围内插入的自增主键它比IDENTITY安全不会被其他连接插入的数据影响。最后UpdateStockAndLog负责加库存和写日志拆成独立方法是为了采购和销售共用一套库存变更逻辑。这段代码不追求花哨但它是进销存系统不烂账的底线。4.2 销售出库条件更新防止并发超卖出库流程和入库方向相反但最大的坑不在“减库存”本身而在“并发超卖”。两个业务员同时看到库存还有 10 件A 下了 8 件B 也下了 8 件如果代码是先查库存、判断够不够、再 update那么两次 update 后库存会变成负数。解决方法是把“判断库存是否充足”和“扣减库存”合并进一条 SQLpublic void CreateStockOut(StockOutEntity main, ListStockOutDetailEntity details) { string connStr ConfigurationManager.ConnectionStrings[ZjxDb].ConnectionString; using (var conn new SqlConnection(connStr)) { conn.Open(); using (var tx conn.BeginTransaction()) { try { string sqlMain INSERT INTO StockOut(OrderNo, CustomerId, OperatorUserId, Remark) VALUES(OrderNo, CustomerId, OperatorUserId, Remark); SELECT SCOPE_IDENTITY();; using (var cmdMain new SqlCommand(sqlMain, conn, tx)) { cmdMain.Parameters.AddWithValue(OrderNo, main.OrderNo); cmdMain.Parameters.AddWithValue(CustomerId, main.CustomerId); cmdMain.Parameters.AddWithValue(OperatorUserId, main.OperatorUserId); cmdMain.Parameters.AddWithValue(Remark, (object)main.Remark ?? DBNull.Value); int stockOutId Convert.ToInt32(cmdMain.ExecuteScalar()); foreach (var d in details) { string sqlDetail INSERT INTO StockOutDetail(StockOutId, ProductId, Qty, Price) VALUES(StockOutId, ProductId, Qty, Price);; using (var cmdDetail new SqlCommand(sqlDetail, conn, tx)) { cmdDetail.Parameters.AddWithValue(StockOutId, stockOutId); cmdDetail.Parameters.AddWithValue(ProductId, d.ProductId); cmdDetail.Parameters.AddWithValue(Qty, d.Qty); cmdDetail.Parameters.AddWithValue(Price, d.Price); cmdDetail.ExecuteNonQuery(); } UpdateStockAndLog(conn, tx, d.ProductId, 2, d.Qty, main.OrderNo, main.OperatorUserId); } } tx.Commit(); } catch { tx.Rollback(); throw; } } } }这里的UpdateStockAndLog内部用的是“条件更新 行锁”的组合private void UpdateStockAndLog(SqlConnection conn, SqlTransaction tx, int productId, int changeType, decimal qty, string orderNo, int operatorUserId) { string sqlUpdate UPDATE dbo.Product WITH (UPDLOCK, ROWLOCK) SET StockQty StockQty DeltaQty WHERE ProductId ProductId; if (changeType 2) // 出库不能把库存扣成负数 { sqlUpdate AND StockQty DeltaQty; } using (var cmdUpdate new SqlCommand(sqlUpdate, conn, tx)) { cmdUpdate.Parameters.AddWithValue(DeltaQty, changeType 2 ? qty : qty); cmdUpdate.Parameters.AddWithValue(ProductId, productId); int affected cmdUpdate.ExecuteNonQuery(); if (changeType 2 affected 0) { throw new Exception(商品库存不足出库失败); } } // 写库存日志BeforeQty 需要先查一次但此时行锁已持有不会产生并发脏读 string sqlLog INSERT INTO StockLog(ProductId, ChangeType, RelatedNo, BeforeQty, ChangeQty, AfterQty, OperatorUserId) SELECT ProductId, ChangeType, OrderNo, StockQty - DeltaQty, DeltaQty, StockQty, OperatorUserId FROM dbo.Product WHERE ProductId ProductId;; using (var cmdLog new SqlCommand(sqlLog, conn, tx)) { cmdLog.Parameters.AddWithValue(ChangeType, changeType); cmdLog.Parameters.AddWithValue(OrderNo, orderNo ?? ); cmdLog.Parameters.AddWithValue(DeltaQty, qty); cmdLog.Parameters.AddWithValue(OperatorUserId, operatorUserId); cmdLog.Parameters.AddWithValue(ProductId, productId); cmdLog.ExecuteNonQuery(); } }WITH (UPDLOCK, ROWLOCK)的意思是更新时对命中行加更新锁和行锁锁粒度小并发下两个出库单不会同时对同一行做“先查后改”。出库的 update 加了StockQty DeltaQty条件SQL Server 在同一个语句里完成“判断 扣减”比先 select 再 update 少一个竞态窗口。如果 affected 为 0说明库存不够直接抛异常让事务回滚。注意写日志时我用StockQty - DeltaQty算出变动前数量StockQty是 update 之后的新值这是利用了同一行数据在锁保护下的稳定性既不用额外查询也不会在并发下算错。4.3 把库存变动收敛到一个存储过程里上面的 C# 代码虽然逻辑清晰但“查库存、判断、更新、写日志”毕竟跨了两次数据库往返。如果采购单有 50 行明细循环里每行都要执行两次命令一个单据提交就是上百次数据库往返。内网环境下还能接受但并不意味着这是最优解。更稳妥的常见做法是把库存变动封装成一个存储过程让事务边界彻底落在数据库里CREATE PROCEDURE dbo.sp_StockChange ProductId INT, ChangeType TINYINT, -- 1入库 2出库 3盘点调整 Qty DECIMAL(18,2), RelatedNo NVARCHAR(32), OperatorUserId INT AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; DECLARE CurrentQty DECIMAL(18,2); SELECT CurrentQty StockQty FROM dbo.Product WITH (UPDLOCK, ROWLOCK) WHERE ProductId ProductId; IF ChangeType 2 AND CurrentQty Qty BEGIN THROW 50001, N库存不足出库失败, 1; END DECLARE Delta DECIMAL(18,2); SET Delta CASE WHEN ChangeType 2 THEN -Qty ELSE Qty END; UPDATE dbo.Product SET StockQty StockQty Delta WHERE ProductId ProductId; INSERT INTO dbo.StockLog (ProductId, ChangeType, RelatedNo, BeforeQty, ChangeQty, AfterQty, OperatorUserId) VALUES (ProductId, ChangeType, RelatedNo, CurrentQty, Delta, CurrentQty Delta, OperatorUserId); COMMIT TRANSACTION; END TRY BEGIN CATCH IF XACT_STATE() 0 ROLLBACK TRANSACTION; THROW; END CATCH; END存储过程方案最大的好处是不依赖 C# 代码里“每一步都记得把事务传进去”的纪律。只要业务方调sp_StockChange库存更新和日志写入天然在一个数据库事务里。C# 层只需要在插入主表和明细时开启一个SqlTransaction循环里调用SqlCommand执行存储过程并把事务对象传给它整体依然保持单事务语义。对于盘点调整、期初建账这种带审计性质的库存变动这个存储过程也能复用只要改ChangeType传入的值即可。5. 进销存系统上线前的五个高频踩坑与排查5.1 GridView 日期显示成 0001/1/1编辑一保存就格式异常现象列表页查看入库日期显示正常一点“编辑”日期列变成0001/1/1或者回发后页面报“字符串未被识别为有效的 DateTime”。原因GridView 的BoundField设置了DataFormatString{0:yyyy-MM-dd}查看模式显示没问题但进入编辑模式后DataFormatString被忽略文本框里拿到的是原始 DateTime 值回发时系统尝试按当前区域设置解析这个值一旦格式不匹配就抛异常。更隐蔽的是数据库字段允许为空时DBNull转成DateTime默认值恰好是0001/1/1。解决编辑模板里显式绑定格式化字符串去掉对BoundField选项的依赖asp:TemplateField HeaderText入库日期 ItemTemplate%# Eval(InDate, {0:yyyy-MM-dd}) %/ItemTemplate EditItemTemplate asp:TextBox IDtxtInDate runatserver Text%# Bind(InDate, {0:yyyy-MM-dd}) %/asp:TextBox /EditItemTemplate /asp:TemplateField5.2 部署到 IIS 后样式全丢、回发跳首页现象本地 Visual Studio 调试一切正常发布到服务器 IIS 后登录页 CSS 和图片全部加载不出来登录成功后跳转到/Default.aspx而不是预期的首页。原因最常见的两种——页面里用了根目录相对路径比如href/css/site.css站点部署在虚拟目录http://ip/zjx/下时这个路径实际指向的是站点根目录下的css文件不存在另一种是 WebForms 的 Forms 认证配置里loginUrl写死了绝对路径没带虚拟目录名。解决统一用ResolveUrl生成带虚拟目录的路径link href%ResolveUrl(~/css/site.css)% relstylesheet /同时检查Web.config中forms loginUrl~/Login.aspx /这里的~会被运行时正确解析为虚拟目录。部署前先在服务器上用浏览器开发者工具看 CSS 请求的实际 URL判断路径差在哪一级。5.3 库存老是差几个数日志缺失和盘点不走流程现象系统跑了一个季度月底盘点发现好几个商品账面比实物少十件查“出入库明细”每个单据都对得上但总数就是不对。原因这个现象八成是库存日志表形同虚设。常见情况是页面里“商品档案维护”可以直接改StockQty字段修库存时没写StockLog另一种是盘点差异直接在数据库里手工 UPDATE没有生成盘点调整单。账面数量对不上的根因不是算错而是存在“没被记录”的变动。解决把库存字段的修改入口全部关掉商品维护页只允许改编码、名称、价格库存调整统一走“盘点调整”功能内部调用sp_StockChange并传ChangeType 3。每月对账用下面这条 SQL 检查商品表库存和日志累计是否一致SELECT p.ProductId, p.ProductCode, p.ProductName, p.StockQty AS CurrentStock, ISNULL(SUM(CASE WHEN sl.ChangeType IN (1,2,3) THEN sl.ChangeQty ELSE 0 END), 0) AS LoggedChange FROM dbo.Product p LEFT JOIN dbo.StockLog sl ON p.ProductId sl.ProductId GROUP BY p.ProductId, p.ProductCode, p.ProductName, p.StockQty HAVING p.StockQty ISNULL(SUM(CASE WHEN sl.ChangeType IN (1,2,3) THEN sl.ChangeQty ELSE 0 END), 0);注意StockLog.ChangeQty里出库类型存的是负值所以 SUM 直接相加就是净变动量。如果这个 SQL 跑出来有行就说明存在绕过日志的库存修改优先查手工 UPDATE 的操作记录。5.4 报表打开就卡死GridView 全量加载与 ViewState 拖垮页面现象库存清单页有几万行数据打开要等十几秒点下一页有时直接超时。查看页面源码发现隐藏字段__VIEWSTATE里塞了几百 KB 的 Base64 字符串。原因SqlDataSource 的 Select 语句是SELECT * FROM ProductGridView 一下加载全表再靠分页控件“切成每页 20 行”——这是假分页数据已经在内存里了ViewState 又把当前页的完整数据状态序列化到表单里。几万行时页面自然变得又慢又大。解决开启数据库层真分页配合ObjectDataSource的SelectMethod返回PagedResult如果源码结构简单至少给 SqlDataSource 加上EnablePagingtrue并配置SelectCountMethod。同时给数据量大的 GridView 设置EnableViewStatefalse页索引和排序状态用 Session 保存。真分页的判断标准是第一行数据只在 SQL 查询时取页面回发时不用再把全表数据留在隐藏字段里。5.5 事务写了对账还是错连接串和事务边界没查清现象代码里明明有BeginTransaction和Commit但异常时空库、明细和库存却出现了“主表有、明细缺几条”的情况。原因最常见的翻车点是循环里又创建了新的SqlConnection实例新连接不在外层事务范围内内部执行的命令各自隐式提交另一种是使用了TransactionScope而环境没有配置 MSDTC跨连接事务静默退化或直接抛异常。事务确实写了只是没写对角。解决先确认整个流程只使用一个SqlConnection实例、一个匹配的SqlTransaction实例所有SqlCommand的构造参数都显式传入这个事务对象。用 SQL Server 的扩展事件或 Profiler 追踪会话观察是否只有一对BEGIN TRAN和COMMIT TRAN。如果业务复杂到需要跨多个数据库优先调整设计让它落在同一个数据库内避免引入分布式事务。6. 给这套源码加分GridView 的 jQuery 体验改造与迁移判断6.1 GridView 三件套分页、导出Excel、操作列如果页面还在用 GridView 且暂时不换框架有三件事花半小时就能提升不少使用感受。第一开启服务端分页设置AllowPagingtrue和PageSize20在PageIndexChanging事件中重置索引并重新绑定数据。第二加导出 Excel 按钮常见做法是把 GridView 渲染成 HTML 表格后设置响应头Response.Clear(); Response.ContentType application/vnd.ms-excel; Response.AddHeader(Content-Disposition, attachment;filename HttpUtility.UrlEncode(库存清单.xls, System.Text.Encoding.UTF8)); StringWriter sw new StringWriter(); HtmlTextWriter htw new HtmlTextWriter(sw); gvMain.RenderControl(htw); Response.Write(sw.ToString()); Response.End();这种导出方式生成的其实是 HTML 文件Excel 打开时提示“格式与扩展名不符”是正常现象直接忽略即可。第三操作列固定放“编辑、删除、查看流水”删除前用confirm弹窗确认避免误删单据。6.2 给 GridView 挂上 jQuery 插件自动补全、日期选择的正确姿势GridView 生成的 HTML 比较朴实但配合 jQuery 插件能补齐体验短板。常见的需求是商品名自动补全和日期选择搜索“asp.net 的 gridview 的 jquery 插件”能找到很多现成方案落地时有一个关键坑要避开WebForms 的 UpdatePanel 局部刷新会覆盖 jQuery 绑定的事件刷新后插件失效。解决方法是把绑定的 JS 写成一个独立函数并在Sys.Application.add_load中调用它。Sys.Application.add_load在 UpdatePanel 每次异步刷新完成后都会触发比document.ready更可靠。比如商品名自动补全Sys.Application.add_load(function () { $(#% txtProductName.ClientID %).autocomplete({ source: function (req, resp) { $.get(/Handlers/ProductSearch.ashx, { term: req.term }, function (data) { resp(JSON.parse(data)); }); }, minLength: 1, select: function (event, ui) { $(#% hidProductId.ClientID %).val(ui.item.id); $(#% txtProductName.ClientID %).val(ui.item.name); return false; } }); });后台用一个一般处理程序ProductSearch.ashx返回 JSON搜索时带 LIKE 条件限制返回前 20 条。需要注意ui.item的具体字段和后台返回结构要对应排查时先打开浏览器开发者工具看接口返回的原始 JSON再调前端字段名。6.3 要不要迁移到 ASP.NET Core三个判断信号最后聊一个每个人拿到这类源码都会问的问题要不要用 ASP.NET Core 重写我的建议是不看技术情怀看三个信号。第一有没有移动端需求老板要求在手机上录单、查库存WebForms 那套回发模型在手机上体验很难救这时候迁到 ASP.NET Core 加 REST API给小程序或 H5 用是正路。第二部署环境变没变客户要求 Linux 或 Docker 容器化WebForms 绑死 Windows 和 IIS迁移是硬性需求。第三团队还有没有后续维护能力如果维护者只会 WebForms业务逻辑也复杂先别动把数据库事务、库存日志、分页性能这些基础打好这套系统还能稳定跑很多年。我接过不少这类进销存系统的维护单栽过最大的跟头是刚入手时急着给 GridView 换主题、加动画把精力花在了表面上结果核心业务单据连“在一个事务里提交”都没做到。先保事务和日志再谈体验这个顺序反了后面全是返工。希望帮到你。本文还有配套的精品资源点击获取
返回列表