☰
C#进销存管理系统实战:从数据库设计到库存预警与打包发布
2026/10/9 3:01:12 网站建设 项目流程

简介:一份基于C#与.NET框架的进销存管理系统完整项目包,面向有C#基础的开发者、计算机专业学生及企业信息化人员,适合作为课程设计、毕业设计或小型进销存业务系统的参考实现。系统围绕采购、销售、库存、报表和财务等核心模块展开,涵盖供应商、客户、订单管理以及出入库跟踪与数据统计分析。整包共109个文件,压缩包大小5.18MB,主要包含49个C#源码文件(.cs)、窗体与资源文件(.resx/.resources)、图标(.ico)、数据库文件(.mdf/.ldf)以及可直接运行的exe,并附有项目解决方案(.sln)和工程配置(.csproj),便于打开编译与二次开发。内容预览中可见采购入库、销售出库、商品信息、员工信息、公司信息、库存查询等多个窗体的设计代码,模块划分清晰。目前已有60人学习下载,适合用于学习WinForms界面设计、ADO.NET或Entity Framework数据访问、SQL Server数据库设计及多层项目结构组织,是一份可直接运行和修改的实战资料。

1. 为什么一个“C#进销存管理系统.zip”值得你解压看看

“C#进销存管理系统.zip”这类压缩包在开发者手里很常见,通常是一个 WinForms 桌面工程的完整源码,商品、供应商、客户、进货、销售、库存流水相关代码和数据库脚本都装在里面,解压后改改连接字符串就能跑。它能帮你省掉从零设计表结构的时间,把增删改查和库存预警的写法直接摊开给你看,像把一套业务骨架送到手边。适合三类人:做课程设计的学生、给小微企业搭内部系统的一线开发、以及想理解 C# 数据访问层的初学者。但这类包往往不是双击即用的成品,数据库版本、连接字符串、事务边界、发布依赖,哪一处没处理好都会翻车。这篇笔记按解压后的落地路径来写,先看数据库设计,再写事务与界面接法,最后集中讲坑和打包流程。

2. 拆开zip先看数据库脚本:进销存的表结构与五张核心表

2.1 一张进货单牵出多少张表

打开 zip 后,我一般不是先看 Form 代码,而是先找带 .sql 后缀的脚本,或者读一下包里 README 对数据库的描述。进销存虽然功能看着多,但核心就围着商品、单据、库存这三件事转。单据分两张主表:进货单和销售单,每种主表又带一张明细表,用来记录一笔单里的多个商品。再加上供应商表、客户表和商品表,这才组成完整的业务闭环。

如果把商品、供应商、客户、进货主表、进货明细、销售主表、销售明细、库存流水统统算进去,一共八张表。通常包里的设计会把这些表拆得详细一点,因为后面要做的库存报表、毛利统计都依赖这些字段。下面是最常见的一组建表脚本,用 SQL Server 语法写的,如果包里带的是 Access 或 MySQL 脚本,字段名基本也差不多,拿过来改一下类型就行。

CREATE TABLE Products ( ProductID INT PRIMARY KEY IDENTITY(1,1), ProductCode NVARCHAR(50) NOT NULL UNIQUE, ProductName NVARCHAR(100) NOT NULL, Spec NVARCHAR(50), Unit NVARCHAR(20), CostPrice DECIMAL(18,2) DEFAULT 0, SalePrice DECIMAL(18,2) DEFAULT 0, StockQuantity DECIMAL(18,2) DEFAULT 0, WarningQuantity DECIMAL(18,2) DEFAULT 0 ); CREATE TABLE Suppliers ( SupplierID INT PRIMARY KEY IDENTITY(1,1), SupplierName NVARCHAR(100) NOT NULL, Contact NVARCHAR(50), Phone NVARCHAR(20) ); CREATE TABLE Customers ( CustomerID INT PRIMARY KEY IDENTITY(1,1), CustomerName NVARCHAR(100) NOT NULL, Phone NVARCHAR(20) ); CREATE TABLE PurchaseOrders ( OrderID INT PRIMARY KEY IDENTITY(1,1), SupplierID INT NOT NULL, OrderDate DATETIME NOT NULL DEFAULT GETDATE(), TotalAmount DECIMAL(18,2) DEFAULT 0, FOREIGN KEY (SupplierID) REFERENCES Suppliers(SupplierID) ); CREATE TABLE PurchaseOrderItems ( ItemID INT PRIMARY KEY IDENTITY(1,1), OrderID INT NOT NULL, ProductID INT NOT NULL, Quantity DECIMAL(18,2) NOT NULL, CostPrice DECIMAL(18,2) NOT NULL, FOREIGN KEY (OrderID) REFERENCES PurchaseOrders(OrderID), FOREIGN KEY (ProductID) REFERENCES Products(ProductID) ); CREATE TABLE SalesOrders ( OrderID INT PRIMARY KEY IDENTITY(1,1), CustomerID INT NOT NULL, OrderDate DATETIME NOT NULL DEFAULT GETDATE(), TotalAmount DECIMAL(18,2) DEFAULT 0, FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID) ); CREATE TABLE SalesOrderItems ( ItemID INT PRIMARY KEY IDENTITY(1,1), OrderID INT NOT NULL, ProductID INT NOT NULL, Quantity DECIMAL(18,2) NOT NULL, SalePrice DECIMAL(18,2) NOT NULL, FOREIGN KEY (OrderID) REFERENCES SalesOrders(OrderID), FOREIGN KEY (ProductID) REFERENCES Products(ProductID) ); CREATE TABLE InventoryFlow ( FlowID INT PRIMARY KEY IDENTITY(1,1), ProductID INT NOT NULL, FlowType CHAR(10) NOT NULL, Quantity DECIMAL(18,2) NOT NULL, RefOrderID INT NOT NULL, FlowDate DATETIME NOT NULL DEFAULT GETDATE(), FOREIGN KEY (ProductID) REFERENCES Products(ProductID) );

建表脚本的逻辑说明:主键统一用 IDENTITY 自增,避免手工给单号,但业务上打印出库单时一般会再生成一个可读的单号,用时间戳加序号拼出来,这个在后面业务代码里说。商品表里 ProductCode 加了 UNIQUE,是为了让条码或物料编码不重复,扫码枪录商品的时候可以直接定位。订单明细表通过外键挂到主表和商品表上,没有把商品名称冗余进来,是因为名称会变,查询时 JOIN 一次性能不算差。

参数说明:DECIMAL(18,2) 表示总长 18 位、小数 2 位,单价、数量都够用,别用 FLOAT 存金额,会有莫名的精度问题。Quantity 用 DECIMAL 而不是 INT,是因为有些行业按重量或长度进货,进货单里可能是 12.5 米、2.3 公斤。WarningQuantity 是库存预警阈值,比如低于 5 时,界面就会标红。

如果包里只有 .mdf 文件而没有 .sql,也别慌。你可以在 SQL Server Management Studio 里右键“数据库”节点,选“附加”,定位到 .mdf 文件。附加后连接字符串要改成AttachDbFilename=|DataDirectory|\Data\MyStock.mdf,这个写法会把数据库文件放在程序相对路径下,发布 zip 时更容易迁移。

2.2 为什么要把库存冗余在商品表里

我在不少包里看到过两种库存设计,一种是商品表里放 StockQuantity,另一种是只靠 InventoryFlow 流水实时求和。第一种是主流,因为它查询快,商品列表、库存预警都直接用字段值,不用反复select sum。第二种适合大数据量分析,但对一个小型进销存来说,每次打开库存列表都去聚合一遍,几千个商品就会明显卡顿。

冗余库存的风险在于:如果库存字段和流水表不一致,根本不知道哪个是真的。所以设计上有一个铁律——任何出入库操作,更新 StockQuantity 和写入 InventoryFlow 必须在同一个事务里完成。这点在第 3 章会写到一个完整例子。另外,如果包里的脚本只有商品表和单据表,没建 InventoryFlow,我一般会自己补一张,不然无法追溯“某个时间点的库存快照”,对账的时候非常痛苦。

还有一点,订单明细表的外键很多人会故意去掉,说是插入慢、删除麻烦。我的做法是前期保留外键,它能挡掉很多脏数据,比如明细里挂一个不存在的商品。真正到了几万行数据连普通查询都吃力,再考虑去掉外键或者上数据库分区,那时候你已经足够熟悉业务了。

2.3 连接字符串写在哪才不会乱改

一个典型的 C# 项目里,连接字符串应该放在 App.config 的 connectionStrings 节,而不是写在每个窗体里。有人图省事直接在 SqlConnection 里写死,等换一台电脑就要全文搜索替换,这是最容易踩坑的点。下面是一段常见配置:

<?xml version="1.0" encoding="utf-8"?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" /> </startup> <connectionStrings> <add name="MainDb" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=MyStock;Integrated Security=True;" providerName="System.Data.SqlClient" /> </connectionStrings> </configuration>

这里 Data Source 用的是.\SQLEXPRESS,这是 SQL Server Express 的默认实例名。如果你装的是完整版 SQL Server,默认实例名一般是计算机名,比如 LAPTOP-ABC,那这里就要改成Data Source=LAPTOP-ABC。Initial Catalog 是数据库名,Integrated Security=True 表示用 Windows 身份登录,在局域网内部系统里用这个最省事,不用在代码里塞密码。

我一般会用ConfigurationManager.ConnectionStrings["MainDb"].ConnectionString取这个值,注意要引用 System.Configuration.dll。如果这个包是从别人的机器拷过来的,最该改的就是 Data Source 这个字段,几乎每一次运行报“连接到数据库”错误,都是因为这里没改。改完连接字符串后,重启程序再试,别在窗体之间切来切去。

3. 看懂和重写数据访问层:从SqlHelper到库存扣减的原子操作

3.1 一个老式SqlHelper就够用

解开 zip,你会发现多数包的逻辑层很薄,直接在 Form 里写 SQL。我们把可复用的数据访问方法抽到一个 SqlHelper 类里。别小看这种老写法,对于进销存这种增删改查为主的项目,它比引入 EF 或 Dapper 更直白,新手也能看懂,出了问题也容易定位。

using System.Configuration; using System.Data; using System.Data.SqlClient; public static class SqlHelper { private static readonly string connStr = ConfigurationManager.ConnectionStrings["MainDb"].ConnectionString; public static DataTable ExecuteQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn = new SqlConnection(connStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (parameters != null && parameters.Length > 0) cmd.Parameters.AddRange(parameters); SqlDataAdapter da = new SqlDataAdapter(cmd); DataTable dt = new DataTable(); da.Fill(dt); return dt; } } public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn = new SqlConnection(connStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } } }

逻辑说明:ExecuteQuery 用于查询,内部用 SqlDataAdapter 把结果填到 DataTable,用完之后 using 会自动关闭连接。ExecuteNonQuery 用于 insert、update、delete,返回受影响行数。这两个方法足够应付进销存 80% 的数据访问场景。

参数说明:params SqlParameter[]允许调用方传任意个参数,比如ExecuteQuery("select * from Products where ProductCode=@code", new SqlParameter("@code", code))。如果传 null,AddRange 会报错,所以要在调用前一并处理。从包里拿到的代码,我见过很多人忘了这一层判断,直接cmd.Parameters.AddRange(parameters),这时传 null 就会炸。

3.2 入库单和销售单必须走事务

进销存最容易出问题的就是库存更新。简单的增删改查各写一行 SQL 没问题,但“创建销售单 + 扣库存 + 写流水”这三个动作合在一起就必须在一个数据库事务里做,否则中途停电或代码异常,会出现订单有了但库存没扣的难堪场面。

下面是一个完整的销售出库方法,进货入库逻辑反过来写就行:

public static bool CreateSaleOrder(int customerId, DataTable items) { // items 列:ProductID, Quantity, SalePrice using (SqlConnection conn = new SqlConnection(connStr)) { conn.Open(); SqlTransaction tran = conn.BeginTransaction(); try { decimal totalAmount = 0; SqlCommand cmd = new SqlCommand(); cmd.Connection = conn; cmd.Transaction = tran; // 1. 插入销售主表 cmd.CommandText = "INSERT INTO SalesOrders (CustomerID, OrderDate, TotalAmount) " + "OUTPUT INSERTED.OrderID VALUES (@cid, @dt, 0)"; cmd.Parameters.AddWithValue("@cid", customerId); cmd.Parameters.AddWithValue("@dt", DateTime.Now); int orderId = (int)cmd.ExecuteScalar(); // 2. 插入明细并计算总额 cmd.Parameters.Clear(); cmd.CommandText = "INSERT INTO SalesOrderItems (OrderID, ProductID, Quantity, SalePrice) " + "VALUES (@oid, @pid, @qty, @price)"; foreach (DataRow row in items.Rows) { cmd.Parameters.Clear(); cmd.Parameters.AddWithValue("@oid", orderId); cmd.Parameters.AddWithValue("@pid", row.Field<int>("ProductID")); cmd.Parameters.AddWithValue("@qty", row.Field<decimal>("Quantity")); cmd.Parameters.AddWithValue("@price", row.Field<decimal>("SalePrice")); cmd.ExecuteNonQuery(); totalAmount += row.Field<decimal>("Quantity") * row.Field<decimal>("SalePrice"); } // 3. 更新主表总额 cmd.Parameters.Clear(); cmd.CommandText = "UPDATE SalesOrders SET TotalAmount = @amount WHERE OrderID = @oid"; cmd.Parameters.AddWithValue("@amount", totalAmount); cmd.Parameters.AddWithValue("@oid", orderId); cmd.ExecuteNonQuery(); // 4. 扣减库存 cmd.Parameters.Clear(); cmd.CommandText = "UPDATE Products SET StockQuantity = StockQuantity - @qty " + "WHERE ProductID = @pid AND StockQuantity >= @qty"; foreach (DataRow row in items.Rows) { cmd.Parameters.Clear(); cmd.Parameters.AddWithValue("@pid", row.Field<int>("ProductID")); cmd.Parameters.AddWithValue("@qty", row.Field<decimal>("Quantity")); int affected = cmd.ExecuteNonQuery(); if (affected == 0) throw new Exception("商品库存不足,扣减失败"); } // 5. 写库存流水 cmd.Parameters.Clear(); cmd.CommandText = "INSERT INTO InventoryFlow (ProductID, FlowType, Quantity, RefOrderID, FlowDate) " + "VALUES (@pid, 'OUT', @qty, @oid, @dt)"; foreach (DataRow row in items.Rows) { cmd.Parameters.Clear(); cmd.Parameters.AddWithValue("@pid", row.Field<int>("ProductID")); cmd.Parameters.AddWithValue("@qty", row.Field<decimal>("Quantity")); cmd.Parameters.AddWithValue("@oid", orderId); cmd.Parameters.AddWithValue("@dt", DateTime.Now); cmd.ExecuteNonQuery(); } tran.Commit(); return true; } catch { tran.Rollback(); throw; } } }

逻辑说明:第 1 步用OUTPUT INSERTED.OrderID在插入主表时直接返回自增主键,省掉一边查一边猜的麻烦。第 2 步循环插入明细并累加总额,第 3 步回填总金额。第 4 步扣库存时故意在 UPDATE 的 WHERE 里带上StockQuantity >= @qty,这样数据库层面就同时完成“扣减”和“校验”,影响行数为 0 就说明库存不足,直接抛异常回滚。

参数说明:AddWithValue 虽然省事,但最好明确指定类型和长度,尤其日期和 decimal。某些包里看到 AddWithValue 把 decimal 当字符串传,性能会变差,极端情况下还会因为隐式转换导致索引失效。如果追求稳,换成new SqlParameter("@qty", SqlDbType.Decimal) { Value = row.Field<decimal>("Quantity") }这种写法。

3.3 参数化查询绕开拼接字符串的兜底

有些包年代比较久,喜欢写string sql = "select * from Products where ProductName='" + name + "'",然后直接 execute。这种写法在内部系统里也能跑,但一旦有人把库存单备注填成' or '1'='1,查询条件就变了,数据会出现“玄学”错乱。严格来说这属于注入,但不是所有包都愿意改。我自己的习惯是,凡是涉及用户输入的查询、更新,一律用参数化。

上面的 SqlHelper 只接受 SqlParameter 数组,所以调用方的 SQL 里出现 @name 就必须对应加参数。例如:

string keyword = txtSearch.Text.Trim(); DataTable dt = SqlHelper.ExecuteQuery( "SELECT ProductCode, ProductName, StockQuantity FROM Products " + "WHERE ProductName LIKE @kw OR ProductCode LIKE @kw", new SqlParameter("@kw", "%" + keyword + "%"));

逻辑说明:LIKE 的模糊匹配符号是写在参数值里的,而不是拼进 SQL,这样参数化继续有效。参数化前后,同一个查询在 SQL Server 里会走参数嗅探,第一次执行生成的执行计划可以被后续复用,性能也更稳。这一步做到了,后面的坑就少一个。

4. 把界面和报表接起来:DataGridView绑定、库存预警与打印条码

4.1 搜索商品时用模糊查询还是扫条码

进销存的日常操作是录单时快速找到商品。条码枪在 Windows 里就是模拟键盘,扫完一串数字自动带个回车。因此我们只要处理文本框的回车事件,在里面执行模糊查询就行。不需要真的去调用串口或者第三方库,这是省钱又省心的做法。

private void txtSearch_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Enter) { string key = txtSearch.Text.Trim(); DataTable dt = SqlHelper.ExecuteQuery( "SELECT ProductID, ProductCode, ProductName, Spec, Unit, SalePrice, StockQuantity " + "FROM Products WHERE ProductCode LIKE @key OR ProductName LIKE @key OR Spec LIKE @key", new SqlParameter("@key", "%" + key + "%")); dgvProducts.DataSource = dt; txtSearch.SelectAll(); } }

逻辑说明:Enter 事件触发查询,SelectAll 方便条码枪连续扫描下一件商品。模糊条件里加 Spec 是因为很多同行经验是“搜索别名”,比如规格里写了“500ml”,输入 500 也能定位到。

参数说明:LIKE 查询在字段上加了前置 %,所以无法利用普通索引,但在几千行的小系统里扫描成本很低,别为这个去改方案。如果未来数据量上到十万行,再考虑用全文索引或者强制要求扫码枪扫精确编码时走WHERE ProductCode=@code的精确查询。

4.2 库存预警的阈值和颜色提醒

商品表的 WarningQuantity 字段就是用来做这个的。界面可以在 DataGridView 上挂一个格式化事件,把低于阈值的行标红,比单独弹窗更直观。颜色提醒作为“视觉警铃”几乎是进销存必须的。

private void dgvProducts_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if (e.RowIndex < 0) return; DataGridViewRow row = dgvProducts.Rows[e.RowIndex]; if (row.Cells["StockQuantity"].Value == null || row.Cells["WarningQuantity"].Value == null) return; decimal stock = Convert.ToDecimal(row.Cells["StockQuantity"].Value); decimal warning = Convert.ToDecimal(row.Cells["WarningQuantity"].Value); if (stock <= warning) { row.Cells["StockQuantity"].Style.BackColor = Color.IndianRed; row.Cells["StockQuantity"].Style.ForeColor = Color.White; } }

逻辑说明:CellFormatting 事件在单元格准备渲染时触发,比较库存和预警阈值。这里要注意提前判断 null,因为新加的行没有值。使用 BackColor + ForeColor 而不是直接设置整行背景,是为了让数字所在列突出,同时保留其他列原始颜色。

参数说明:这种写法只影响当前界面,数据没变。如果需要把预警结果固化到报表里,可以在查询里加一列 IsWarning:CASE WHEN StockQuantity <= WarningQuantity THEN 1 ELSE 0 END,后续做统计就方便。

4.3 把 DataGridView 内容存成 TXT 或 CSV 的一行代码方案

后台常有人要导出库存列表,但老板往往不想装 Excel,那么直接导出成 TXT 最通用。这里给一个能直接用的代码,把 DataGridView 当前显示的表格保存为逗号分隔文件,也是热搜里经常被翻到“c# listview项保存到txt文件”的同类需求。

private void btnExport_Click(object sender, EventArgs e) { SaveFileDialog dlg = new SaveFileDialog(); dlg.Filter = "TXT 文件|*.txt"; if (dlg.ShowDialog() != DialogResult.OK) return; using (StreamWriter sw = new StreamWriter(dlg.FileName, false, Encoding.UTF8)) { for (int i = 0; i < dgvProducts.Columns.Count; i++) { sw.Write(dgvProducts.Columns[i].HeaderText); if (i < dgvProducts.Columns.Count - 1) sw.Write(","); } sw.WriteLine(); foreach (DataGridViewRow row in dgvProducts.Rows) { for (int i = 0; i < dgvProducts.Columns.Count; i++) { string value = row.Cells[i].Value?.ToString() ?? ""; if (value.Contains(',')) value = "\"" + value + "\""; sw.Write(value); if (i < dgvProducts.Columns.Count - 1) sw.Write(","); } sw.WriteLine(); } } MessageBox.Show("导出完成"); }

逻辑说明:先写列头,再逐行写单元格值,值里如果带逗号就加双引号包裹,保证打开后不乱行。用 Encoding.UTF8 而不是默认 ANSI,否则 Windows 记事本打开中文会乱码。

参数说明:StreamWriter 的第二个参数 false 表示覆盖写入;如果真要追加日志可以改成 true。这里只演示导出当前 DataGridView 数据,如果要导出原始查询数据,应该再调用 SqlHelper 拿新的 DataTable,别依赖界面上的显示数据,因为界面可能做了排序和滤除。

4.4 给录单界面加上“先选商品再填数量”的交互

录进货单和销售单时,如果直接在 DataGridView 里逐行编辑,很容易把数量填到错误的行。我见过很多包的做法是:左边商品查询列表,右边订单明细列表,双击左表格中的一行,就把商品加到右表格,数量默认 1,再在右表格里改数量。这个交互的代码核心就是两个 DataGridView 和一个方法。

private void dgvProducts_DoubleClick(object sender, EventArgs e) { if (dgvProducts.CurrentRow == null) return; string productId = dgvProducts.CurrentRow.Cells["ProductID"].Value.ToString(); string name = dgvProducts.CurrentRow.Cells["ProductName"].Value.ToString(); decimal price = Convert.ToDecimal(dgvProducts.CurrentRow.Cells["SalePrice"].Value); foreach (DataGridViewRow row in dgvOrderItems.Rows) { if (row.Cells["ProductID"].Value?.ToString() == productId) { row.Cells["Quantity"].Value = Convert.ToInt32(row.Cells["Quantity"].Value) + 1; return; } } dgvOrderItems.Rows.Add(productId, name, 1, price, price); }

逻辑说明:双击左侧商品行,先检查右侧明细是否已包含该商品,有就把数量加 1,没有就新增一行。这个“重复扫码自动累加”的做法很适合零售场景,也避免了同一商品在明细里出现两行。

参数说明:dgvOrderItems 的列需要在设计器里编辑好,ProductID、ProductName 这些列可以设为只读,只让 Quantity 可编辑。总额那列如果希望自动算,可以在 CellValueChanged 里重算,但注意事件会在程序初始化和绑定数据时频繁触发,最好加一个isLoading开关。

5. 几个让人翻车的坑:从数据库版本到外键约束

5.1 报错“无法连接到数据库” / LocalDB实例找不到

现象:解压运行 exe,直接弹 SqlConnection 错误,说“在建立与服务器的连接时出错”或者“找不到 LocalDB 实例”。

原因:包里 App.config 写的是Data Source=.\SQLEXPRESS或(LocalDB)\MSSQLLocalDB,而你机器上只装了 SQL Server Express 但实例名不同,或者根本没装。

解决:打开 Windows 服务管理器,看 SQL Server 服务名。如果连接字符串指向 LocalDB,需要先确认是否安装了 SQL Server Express LocalDB,或者去“程序和功能”里查。最省事的办法是把 connectionString 改成Data Source=.,让它连本机默认实例,再检查数据库文件是否已附加。

5.2 删商品资料时被外键挡住

现象:商品已经不被订单引用,但删除还是失败,提示“与 REFERENCE 约束冲突”。

原因:明细表或流水表里残留历史记录,外键禁止删除父表记录。

解决:不要物理删除商品,给 Products 加一个 IsActive 字段,查询列表时默认只显示 IsActive=1。这比删数据安全,也是我最后悔没在早期采用的做法——曾强制删流水,结果把对账数据弄没了。

5.3 库存出现负数

现象:做销售出库时,库存反而多扣了;或者录单时多敲了一位数量,库存变负数。

原因:扣减库存的 UPDATE 没有判断库存量,或者数据库被多端同时操作,读取到的库存是旧值。

解决:第 3 章事务里用的UPDATE ... SET StockQuantity = StockQuantity - @qty WHERE StockQuantity >= @qty就是一种防呆设计。如果业务允许负库存,那另说;但销售出库默认不允许负库存。再进一步,可以在扣减外层加事务隔离级别或者SELECT ... WITH (UPDLOCK, ROWLOCK)锁住商品行,防止并发超卖。

5.4 中文显示成乱码“????”

现象:程序里看到的商品名是问号,数据库里也是问号。

原因:导入 SQL 脚本时用了非 UTF8 编码,或者表排序规则不是中文相关,从 GBK 转 UTF8 时丢了字符。

解决:用记事本打开 .sql 另存为 UTF-8 with BOM,再导入。建表时把 varchar 换成 nvarchar,排序规则手动指定 Chinese_PRC_CI_AS。已经乱掉的数据只能改编码后重新插入,别指望 UPDATE 能找回。

5.5 发布后的 zip 解压到别的电脑仍然报错

现象:在自己电脑一切正常,打包成 zip 发给同事,解压后能打开却连不上数据库。

原因:数据库是独立服务,没有随包走;连接字符串用的是本机实例名;也可能代码引用了某个第三方 DLL,发布模式用的框架依赖不全。

解决:发布时在 Visual Studio 选择 Release、AnyCPU,目标框架选择“自包含”或“框架依赖”要确认好。如果包要求轻量,我通常把 SQL Server Express LocalDB 的 .mdf 文件放进项目的 Data 目录,连接字符串写AttachDbFilename=|DataDirectory|\MyStock.mdf,并在程序中设置AppDomain.CurrentDomain.SetData("DataDirectory", 程序目录),这样数据库文件就能随 zip 走。

6. 打包成zip前的最后几步:发布配置与验证清单

拿到“C#进销存管理系统.zip”不一定要先解压。如果是源码包,你要做的是打开 .sln 重新编译;如果是发布包,直接看 exe。我自己的发布习惯是:先在 Visual Studio 里把解决方案配置切换为 Release、AnyCPU,然后右键项目选择“发布”——如果你是 .NET Framework 项目,发布向导会生成一个文件夹,里面有 exe、dll、config;把数据库脚本和 README 一并放进去,最后把整个目录压缩成 zip。

压缩时的关键点是保持目录结构,别把 exe 直接扔到 zip 根目录而不带依赖文件。很多人犯了错,解压后双击 exe 报“初始化失败”,就是因为 DLL 找不到。用压缩软件右键“添加到压缩文件”时,默认会把一级目录名带进去,解压后注意路径别多套一层。

验证一个发布包是否可靠,我一般会在一台“干净”的虚拟机或同事电脑上做三件事:第一,解压 zip,确认文件结构里至少包含 exe、config,以及数据库文件或脚本;第二,改连接字符串或附加数据库文件,启动程序;第三,走一遍完整业务流:建供应商、建商品、入库、出库、看库存变化、看流水。如果三次都能正确回退,才算这个 zip 真正能交付。

如果你只是想把源码压缩包转成自己能跑的工程,我的教训是:先备份数据库源文件,再改代码。我曾经为了加一个字段直接改了表结构,结果把包里附带的演示数据全弄坏了,后来才知道大型项目会把 .bak 放一份在 zip 的 database 备份目录里。现在我会在发布 zip 时额外放一个 restore.bat 或 restore.sql,这样换电脑后能一键恢复数据库。希望这些经验帮到你。

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

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

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

立即咨询