资讯动态

C#进销存管理系统实战:从零搭建WinForms+SQLite库存管理

发布时间:2026/10/1 17:21:38 来源:尧图企业网站定制
简介这是一套基于C#与.NET框架开发的进销存管理系统完整项目源码面向C#初学者、毕业设计学生及有小型进销存需求的企业开发者覆盖采购、销售、库存、报表分析等核心业务环节。压缩包共109个文件以C#源文件.cs、窗体设计文件.resx及Designer.cs和资源文件.resources、.ico等为主体同时包含可直接运行的exe程序、PDB调试符号及SQL Server数据库文件.mdf/.ldf整体大小约5.18MB。资源内置供应商、客户、商品信息管理及入库、出库、盘点等模块并附带项目解决方案与编译缓存文件便于直接打开学习或二次开发。通过研读代码可掌握WinForms界面布局、Entity Framework数据映射、多线程数据刷新及异常处理等C#桌面应用开发技巧已有58人浏览学习适合作为课程设计或业务系统开发的参考资料。1. 为什么 2025 年还要碰 C# 进销存管理系统这个 zip 里到底装了什么C# 进销存管理系统.zip这类压缩包在技术社区流传了十多年。下载下来解压多半是一个 2010 年前后创建的 Visual Studio 解决方案里面躺着几个项目、一堆窗体文件和一张 SQL 脚本。它的业务是进货、销售、库存三者联动入库单把商品加进库存销售单把库存减掉低于预警线就提示补货。别看不起这套东西——中小企业门店、小型贸易商、甚至工厂的备件库到今天仍然在用类似逻辑的系统管库存而 C# 是这类项目里代码量最低、招人最容易、交付后最好维护的技术栈之一。这篇笔记面向三类人想拿现成源码改一版给自己门店用的从业者刚转 .NET 想找个完整项目练手的开发者以及被老板要求一周内搞个进销存的倒霉工程师。我会按自己的交付习惯把选型、库表设计、最小可运行代码、高频踩坑点完整拆开你照着走就能跑通一个能用的版本。2. 动手前的选型WinForms、SQLite 与三层架构怎么定打开 zip 之前先决定这版系统用什么底子。选型决定了你后面是顺风顺水还是反复返工。我在这一章把界面层、数据库、代码结构三个关键选择讲清楚并给出场景化建议你要做的不是在最新技术里挑而是在最不容易翻车里挑。2.1 界面层WinForms 还是 WPF没有标准答案很多老 zip 里是 WinFormsSystem.Windows.Forms因为 2008 年到 2015 年之间这是 .NET 桌面开发的主流。它的优点是控件拖拽即用、DataGridView 绑定 DataTable 两行代码出表格、学习曲线平缓缺点是界面观感停留在上世纪高分屏缩放偶尔模糊样式定制能力弱。WPFWindows Presentation Foundation的 XAML 布局更灵活界面现代化支持数据绑定、样式模板、虚拟化列表适合做稍微像样的客户界面。但它的学习成本明显更高——依赖属性、绑定上下文、触发器这些概念新手第一周基本在跟绑定失败作斗争。我的建议你只是给自己或小门店做个内部工具数据量不大UI 没人打分——选 WinForms快就是正义。代码包里的范例也多是 WinForms改起来省力。你要做的是一个要见客户、要持续迭代一两年的产品——选 WPF后面换肤、加图表、做导航栏都方便。但启动成本会高出 30% 左右按开发工期估算。两个人以上同时维护不管选哪个务必把 UI 代码和业务代码分开。最常见的翻车就是窗体里连数据库、按钮里写 SQL最后改需求时没人敢动那个 5000 行的 Form1.cs。以我接手过的几个小进销存项目看单机版用 WinForms 的占比极高几乎全是 2015 年前的老底子近三年新写的基本转向 WPF 或 Web。你如果下载的 zip 是老结构我不建议强行迁移到 WPF第一版能跑比好看更重要。2.2 数据库SQLite 起步SQL Server 上生产进销存的核心资产是数据数据库选错后面全是泪。这类系统常见的库有三个SQLite、SQL Server、MySQL。它们各有各的场景我按单人小门店 → 多客户端局域网 → 需要分权限/海量数据三个档位给结论单机版、门店收银级并发、不想装数据库服务——SQLite。它是文件型数据库整个库就是一个 .db 文件备份就是复制文件连接字符串一行搞定不需要配置服务实例对低配电脑友好。很多 zip 源码里就是 SQLite因为附带的System.Data.SQLite.dll或后来微软官方的Microsoft.Data.Sqlite都能直接引用。三五个客户端同时操作、需要正经的账号权限体系——SQL Server Express。它是服务型数据库本地开发模式和部署模式统一支持行版本控制、事务日志、备份还原出了问题资料多。缺点是内存占用和安装门槛比 SQLite 高首次部署需要装实例、配端口、开防火墙。已有 MySQL 环境或 Web 端复用技术栈——MySQL。若你的公司 DBA 只在 MySQL 上有运维经验别为了统一硬上 SQL Server。开发时注意 SQL 方言差异比如自增列写法、自动生成的时间戳其余差别不大。我自己的通用套路是开发期一律 SQLite统一连接字符串写法交付时如果客户是多点并发改成 SQL Server数据量极大才考虑 MySQL 分库。这样做的好处是前期不用装一堆服务代码层面换库只需要换连接串和少数 SQL 方言实践中 SQL Server 和 SQLite 在标准 CRUD 上的差异控制在 5% 以内。2.3 三层架构是及格线不是加分项很多 C# 进销存源码的结构是Form 代码里拼 SQL、后台线程里连数据库、订单按钮里同时做十件事。这种代码维护半年后改一行销售逻辑可能牵出三个 bug。正确做法是三层别嫌它啰嗦界面层UI窗体 控件绑定只负责展示和收集输入不碰 SQL。业务层BLL处理订单、库存、结算规则。比如下单时库存不足则报错并回滚写在这里不散落在按钮事件里。数据访问层DAL封装所有 ADO.NET 或 ORM 操作提供GetProductById()、InsertSaleOrder()之类的方法。换数据库时只改这一层。在代码组织上我会在解决方案里放三个项目Inventory.UIWinForms 或 WPF、Inventory.BLL、Inventory.DAL另外加一个Inventory.Models放实体类。如果你下载的 zip 只有一个项目且所有代码挤在一个 cs 文件里第一件事就是先按这个结构拆开再改业务否则后面每次调试都是地狱。三层架构值不值得为小门店系统做全套值得。哪怕你只写了两个实体、三个窗体、五个业务方法分层也能让你在新加报表功能时不心惊肉跳。数据访问层里我用 Dapper 居多一个轻量 ORM拿 SQL 和参数直接映射实体性能好、可控性强不太推荐给老项目引入 EF Core 全家桶——迁移和学习成本高抵得过你省下的那几行增删改查代码吗不适合杀鸡不用牛刀。3. 表结构设计与库存联动一张进货单如何改三张表进销存系统的成败不在界面在库表设计。一个典型的进货流程采购入库单登记了商品、数量、单价、供应商库存表要加库存资金流水要记录应付商品表可能还要更新最近进价。任何一步漏掉账就对不上。这一章给出一套可落地的表结构并解释库存联动的数据流向。3.1 核心表商品、供应商、客户与库存少一张就会出乱子先明确最基础的实体。对于一个通用进销存最小集是四张主数据表商品表、供应商表、客户表、库存表。下面用 SQLite 的标准写法给出建表脚本你换到 SQL Server 时只需改列类型和自增写法。-- 商品主数据 CREATE TABLE Products ( Id INTEGER PRIMARY KEY AUTOINCREMENT, -- 内码自增 ProductCode TEXT NOT NULL UNIQUE, -- 商品编码前台扫码用 Name TEXT NOT NULL, -- 商品名称 Spec TEXT, -- 规格型号 Unit TEXT DEFAULT 件, -- 计量单位 SalePrice DECIMAL(12,2) DEFAULT 0, -- 销售价 LastInPrice DECIMAL(12,2) DEFAULT 0, -- 最近进价 MinStock DECIMAL(12,2) DEFAULT 0, -- 库存预警下限 Remark TEXT ); -- 供应商 CREATE TABLE Suppliers ( Id INTEGER PRIMARY KEY AUTOINCREMENT, SupplierCode TEXT NOT NULL UNIQUE, Name TEXT NOT NULL, Contact TEXT, Phone TEXT ); -- 客户 CREATE TABLE Customers ( Id INTEGER PRIMARY KEY AUTOINCREMENT, CustomerCode TEXT NOT NULL UNIQUE, Name TEXT NOT NULL, Contact TEXT, Phone TEXT ); -- 库存表一种商品一条记录 CREATE TABLE Stock ( Id INTEGER PRIMARY KEY AUTOINCREMENT, ProductId INTEGER NOT NULL UNIQUE, Quantity DECIMAL(12,2) DEFAULT 0, -- 当前库存量 UpdatedAt TEXT DEFAULT (datetime(now,localtime)), FOREIGN KEY (ProductId) REFERENCES Products(Id) );注意点有三个。第一金额统一用DECIMAL(12,2)绝对不用float或double原因放第 5 章避坑里细讲。第二库存单独拆一张表而不是在 Products 里加StockQuantity字段。拆开的好处是库存变动频繁更新库存不需要动商品主数据对账时直接查 Stock 表统计商品资料时又不被库存数据拖慢。第三ProductCode 这类对外用的编码要建唯一索引避免重复录入内部 Join 全用自增的 Id别拿编码当主键——编码可能会改Id 不会。一个常见问题是库存表里要不要记录仓库。如果你的业务有多个仓库或多个门店Stock 表加一个WarehouseId外键即可查询时按ProductId WarehouseId联合唯一。没有多仓需求就别加加了纯属给自己找麻烦。3.2 流水表入库单、销售单、库存变动记录账才能对得上主数据表解决有什么流水表解决发生了什么。进销存的最小流水集合是三张采购入库单、销售出库单、库存变动流水。后面要算毛利、统计周转都依赖这些流水。-- 采购入库单一单头 多明细 CREATE TABLE PurchaseOrders ( Id INTEGER PRIMARY KEY AUTOINCREMENT, OrderNo TEXT NOT NULL UNIQUE, -- 单号如 PO20250612001 SupplierId INTEGER NOT NULL, OrderDate TEXT DEFAULT (datetime(now,localtime)), TotalAmount DECIMAL(12,2) DEFAULT 0, -- 整单金额冗余方便对账 Remark TEXT, FOREIGN KEY (SupplierId) REFERENCES Suppliers(Id) ); CREATE TABLE PurchaseOrderItems ( Id INTEGER PRIMARY KEY AUTOINCREMENT, OrderId INTEGER NOT NULL, ProductId INTEGER NOT NULL, Quantity DECIMAL(12,2) NOT NULL, -- 入库数量 UnitPrice DECIMAL(12,2) NOT NULL, -- 本次进价 FOREIGN KEY (OrderId) REFERENCES PurchaseOrders(Id), FOREIGN KEY (ProductId) REFERENCES Products(Id) ); -- 销售出库单 CREATE TABLE SaleOrders ( Id INTEGER PRIMARY KEY AUTOINCREMENT, OrderNo TEXT NOT NULL UNIQUE, -- 如 SO20250612001 CustomerId INTEGER NOT NULL, OrderDate TEXT DEFAULT (datetime(now,localtime)), TotalAmount DECIMAL(12,2) DEFAULT 0, Remark TEXT, FOREIGN KEY (CustomerId) REFERENCES Customers(Id) ); CREATE TABLE SaleOrderItems ( Id INTEGER PRIMARY KEY AUTOINCREMENT, OrderId INTEGER NOT NULL, ProductId INTEGER NOT NULL, Quantity DECIMAL(12,2) NOT NULL, -- 出库数量 UnitPrice DECIMAL(12,2) NOT NULL, -- 成交单价 FOREIGN KEY (OrderId) REFERENCES SaleOrders(Id), FOREIGN KEY (ProductId) REFERENCES Products(Id) ); -- 库存变动流水只追加不修改不删除 CREATE TABLE StockLogs ( Id INTEGER PRIMARY KEY AUTOINCREMENT, ProductId INTEGER NOT NULL, ChangeQty DECIMAL(12,2) NOT NULL, -- 正数入库负数出库 Balance DECIMAL(12,2) NOT NULL, -- 变动后的库存余额 RefType TEXT NOT NULL, -- PURCHASE / SALE / ADJUST RefId INTEGER NOT NULL, -- 关联单据明细 Id CreatedAt TEXT DEFAULT (datetime(now,localtime)), FOREIGN KEY (ProductId) REFERENCES Products(Id) );这套结构的关键在 StockLogs。它记录了每一次库存变动的前后余额既能追溯这箱货去哪了也是将来做日结报表和库存对账的依据。RefType 加 ADJUST 是为了支持盘点和报损这三类流水在界面上分别做入口。StockLogs 只允许 Insert不允许 Update 和 Delete——这是审计纪律在代码层就要把改流水这个操作从逻辑上禁掉。3.3 入库与出库的库存联动事务里两步走有了表结构业务逻辑的核心就是保存单据时同事务内写单头、写明细、更新库存。以采购入库为例一个正确的入库操作包含三步缺一不可写 PurchaseOrders 和 PurchaseOrderItems按商品更新 Stock 表库存Quantity 加上本次数量写 StockLogs 流水Balance 取更新后的值。如果这三步中间任何一步失败必须整体回滚否则会出现单据存了但库存没变的脏账。很多老 zip 源码在这里就偷懒:先 Insert 订单再循环 Update 库存中间没有事务。一旦第二件商品更新失败第一件商品的库存已经动了账从此开始烂。使用 ADO.NET 手动管理事务时标准写法是这样的Dapper 提供的 IDbTransaction 可以无侵入地挂到命令上// 伪代码级完整示例方法名与变量名按实际项目替换 public void CreatePurchaseOrder(PurchaseOrder order, ListPurchaseOrderItem items) { const string sqlOrder INSERT INTO PurchaseOrders(OrderNo, SupplierId, OrderDate, TotalAmount, Remark) VALUES(OrderNo, SupplierId, OrderDate, TotalAmount, Remark); SELECT last_insert_rowid();; const string sqlItem INSERT INTO PurchaseOrderItems(OrderId, ProductId, Quantity, UnitPrice) VALUES(OrderId, ProductId, Quantity, UnitPrice);; // 条件更新库存足够才扣减affected0 表示不足 const string sqlUpdateStock UPDATE Stock SET Quantity Quantity Qty, UpdatedAt datetime(now,localtime) WHERE ProductId ProductId;; const string sqlGetBalance SELECT Quantity FROM Stock WHERE ProductId ProductId;; const string sqlStockLog INSERT INTO StockLogs(ProductId, ChangeQty, Balance, RefType, RefId) VALUES(ProductId, Qty, Balance, PURCHASE, ItemId);; using var conn new SqliteConnection(_connString); conn.Open(); using var tx conn.BeginTransaction(); try { // 1. 写单头拿到自增单头 Id int orderId conn.ExecuteScalarint(sqlOrder, order, tx); // 2. 循环写明细每写一条明细就联动更新库存并记录流水 foreach (var item in items) { item.OrderId orderId; int itemId conn.ExecuteScalarint(sqlItem, item, tx); // 拿到明细 Id int affected conn.Execute(sqlUpdateStock, new { ProductId item.ProductId, Qty item.Quantity }, tx); if (affected 0) throw new Exception($商品 {item.ProductId} 未在 Stock 表中初始化入库失败); decimal balance conn.ExecuteScalardecimal(sqlGetBalance, new { ProductId item.ProductId }, tx); conn.Execute(sqlStockLog, new { ProductId item.ProductId, Qty item.Quantity, Balance balance, ItemId itemId }, tx); } tx.Commit(); } catch { tx.Rollback(); throw; } }这个例子的关键参数与设计点BeginTransaction()之后的所有Execute都要显式传入tx否则这些命令会跑到隐式事务里回滚管不到它。这是新手最容易忽略的一行。入库用Stock Stock Qty而不是先查后写。先查再写会出现两个并发入库把增量覆盖的情况。直接累加是原子操作多线程同时入库也不会丢更新。明细写完后立刻写流水Balance 现查出来的值保证了流水里的余额和当前库存一致。这里的查询和更新在同一事务内读到的就是本事务的可见版本。销售出库则反着来更新库存的 SQL 改成SET Quantity Quantity - Qty WHERE ProductId ProductId AND Quantity Qty受影响行数为 0 就说明库存不足直接抛异常回滚。用AND Quantity Qty比先查后判断更严谨它把库存是否够和扣减库存合并成一个原子操作从根上避免超卖。库存初始化也要注意凡是在界面上新增商品保存的同时必须在 Stock 表插一条Quantity 0的记录。没有这条初始记录上面代码里的更新语句 affected 永远是 0商品永远入库失败。4. 跑通最小系统连接、建库、CRUD 到订单提交理论讲清楚后我们进入照着敲就能跑阶段。这一章我用 SQLite WinForms Dapper 搭一个最小可运行的进销存雏形目标是跑通一条链添加商品 → 建采购入库单 → 库存增加 → 建销售单 → 库存扣减。你把这套跑通了再去套自己项目的业务规则。4.1 准备项目与连接字符串这两行配置决定了调试效率创建一个 .NET 8或 .NET Framework 4.7.2如果老 zip 里的依赖不支持新运行时的 WinForms 项目通过 NuGet 安装Microsoft.Data.Sqlite微软官方 SQLite 驱动Dapper轻量 ORM只做对象映射不抢 SQL 控制权连接字符串集中写在 App.config 或 appsettings.json 里。我的习惯是放两个连接串Debug 模式指向开发库文件Release 模式下可以从配置文件动态替换这样部署时不用重新编译。!-- App.config 示例 -- connectionStrings !-- 开发库每次调试都用这个文件方便复查 -- add nameInventoryDb connectionStringData SourceInventoryDev.db;ModeReadWriteCreate;Foreign KeysTrue providerNameMicrosoft.Data.Sqlite / !-- 发布后替换成客户端机器上的实际路径 -- /connectionStrings连接字符串里两个参数值得说明。Data Source写的是数据库文件路径相对路径是相对于程序运行目录建议发布时改成绝对路径否则客户端从不同目录启动会连到不同的库数据消失的玄学多半源于此。Foreign KeysTrue必须显式打开——SQLite 默认不开启外键约束忘了写你建的那些 FOREIGN KEY 全是摆设删掉被引用的商品不会被拦截数据完整性无从谈起。4.2 实体类与数据访问层用 Dapper 把表和 C# 对象对齐实体类的字段名尽量和表列名一致Dapper 默认按名称匹配不一致时要写[Column]映射徒增维护成本。下面给出最小实体类和数据访问层// Models/Product.cs public class Product { public int Id { get; set; } public string ProductCode { get; set; } public string Name { get; set; } public string Spec { get; set; } public string Unit { get; set; } public decimal SalePrice { get; set; } public decimal LastInPrice { get; set; } public decimal MinStock { get; set; } public string Remark { get; set; } } // Models/PurchaseOrderItem.cs public class PurchaseOrderItem { public int Id { get; set; } public int OrderId { get; set; } public int ProductId { get; set; } public decimal Quantity { get; set; } public decimal UnitPrice { get; set; } }// DAL/InventoryRepository.cs —— 只放数据访问不放业务规则 public class InventoryRepository { private readonly string _connString; public InventoryRepository(string connString) { _connString connString; } public ListProduct GetProducts(string keyword null) { const string sql SELECT Id, ProductCode, Name, Spec, Unit, SalePrice, LastInPrice, MinStock, Remark FROM Products WHERE (Keyword IS NULL OR Name LIKE % || Keyword || % OR ProductCode LIKE % || Keyword || %) ORDER BY ProductCode;; using var conn new SqliteConnection(_connString); return conn.QueryProduct(sql, new { Keyword keyword }).ToList(); } public int InsertProduct(Product p) { const string sql INSERT INTO Products(ProductCode, Name, Spec, Unit, SalePrice, LastInPrice, MinStock, Remark) VALUES(ProductCode, Name, Spec, Unit, SalePrice, LastInPrice, MinStock, Remark); SELECT last_insert_rowid();; using var conn new SqliteConnection(_connString); // 这里开始没开事务因为单条插入本身是原子的 return conn.ExecuteScalarint(sql, p); } // 其余 GetSupplier、GetCustomer、InsertSaleOrder 等按相同模式扩展 }说三个使用细节。SELECT last_insert_rowid()是 SQLite 拿自增主键的方式SQL Server 用SCOPE_IDENTITY()MySQL 用LAST_INSERT_ID()。这是换库时最需要改的方言点之一。查询里用(Keyword IS NULL OR ...)的写法目的是让不传关键字查全表和传关键字过滤共用一条 SQL避免在 C# 里拼字符串时引入注入风险。每个方法内using var conn保证连接用后即关Dapper 的QueryT/Execute是扩展方法会自动管理打开关闭状态。写入多表时记得用事务但单表插入不要画蛇添足开事务纯属浪费。4.3 窗体绑定与线程留给 UI 的三个红线WinForms 窗体的标准做法是Load 事件里查一次数据源绑定到 DataGridView用户点新增保存时调用 BLL 方法成功后再重新查询刷新界面。示例不做全套只给最关键的绑定模式// UI/MainForm.cs节选 private readonly InventoryRepository _repo; private BindingListProduct _productList; public MainForm() { InitializeComponent(); var connStr ConfigurationManager.ConnectionStrings[InventoryDb].ConnectionString; _repo new InventoryRepository(connStr); } private void MainForm_Load(object sender, EventArgs e) { RefreshProductGrid(); } private void RefreshProductGrid() { var list _repo.GetProducts(txtKeyword.Text.Trim()); _productList new BindingListProduct(list); dgvProducts.DataSource _productList; // 简单直接小数据量无压力 } private void btnSaveProduct_Click(object sender, EventArgs e) { var product new Product { ProductCode txtCode.Text.Trim(), Name txtName.Text.Trim(), Spec txtSpec.Text.Trim(), SalePrice decimal.Parse(txtSalePrice.Text.Trim()), MinStock decimal.Parse(txtMinStock.Text.Trim()) }; _repo.InsertProduct(product); RefreshProductGrid(); // 刷新比做局部更新更省心 }这段代码背后有三条红线务必记住。不要在 UI 线程里执行超过几百毫秒的数据库操作。单机小库感觉不出来换了 SQL Server 且有大量数据时界面会假死用户会以为程序崩了。标准解法是Task.Run包一层配合BeginInvoke回到 UI 线程刷新控件。decimal.Parse在用户输入非法时直接抛异常界面体验很差。生产级做法是用decimal.TryParse失败时给提示并 return不要指望用户永远输入合法数字。DataGridView 绑定的是BindingListT而不是ListT。前者在数据变更后能自动通知界面刷新后者绑上去是静态快照你改了集合界面纹丝不动误导性极强。跑完上面三步你已经有一个能增删查商品的最小程序了。下一步把第 3 章事务版入库方法接进来再在窗体上放入库单和销售单两个按钮这条链路就通了。整个最小系统预计一天之内能搭完剩下的时间全花在业务细节和报表上。5. 进销存避坑实录金额、并发、编码与老代码包的五处翻车这套系统看着简单实际跑起来翻车点密集集中在五个地方小数精度、并发扣减、日期与编码、老 zip 打不开、以及单号前导零丢失。每一条都是我在真实项目里踩过的按现象 → 原因 → 解决写给你省得你再走一遍。5.1 金额错乱float 存储小数点后两位对不齐现象采购 100 件商品单价 0.1 元总金额算出来是 10.000000000002 元报表合计有时比明细多一分钱客户对账对不上。原因二进制浮点数无法精确表示十进制小数。0.1 在 float/double 里是无穷循环的近似值累加误差在报表阶段集中爆发。解决所有金额、数量、单价字段一律DECIMAL(12,2)C# 实体对应decimal禁止用double和float接收。运算时不要先转 double 再转回 decimal全程保持 decimal 类型。另外单价和数量算金额时用Math.Round(quantity * unitPrice, 2)显式指定舍入规则避免银行家舍入和四舍五入的差异影响账目。订单表里我特意冗余了TotalAmount就是给对账和列表页一个直接可用的金额避免每次报表都去重新求和明细。5.2 超卖与库存负数扣减库存时并发打架现象两个收银台同时卖最后一件商品两个订单都显示成功库存变成 -1。单用户门店测不出来多人同时操作就现形。原因先查询库存数量判断足够再执行 Update 扣减两步之间存在时间窗口第二个事务读到的是扣减前的老值。解决把判断和扣减合并成一条原子 SQL这是我在 3.3 节写过的路子。销售出库用UPDATE Stock SET Quantity Quantity - Qty WHERE ProductId ProductId AND Quantity Qty受影响行数为 0 立即回滚并提示库存不足。再用事务把订单写入和库存扣减包在一起确保要么订单和库存都成功要么都失败。库存允许负数的系统都是定时炸弹宁可让销售失败也不让账烂。5.3 中文乱码与 CSV 导出Excel 打开全是乱码现象程序里显示正常的中文用记事本打开数据库文件是乱码导出的 CSV 在 Excel 里中文全变问号。原因SQLite 存储文本默认 UTF-8程序里正确显示不代表文件层面肉眼可读这是正常的真正的问题是导出 CSV 时用了 ANSI 或 UTF-8 无 BOM 编码Excel 默认按 ANSI 猜测解码中文就炸了。解决连接字符串加Foreign KeysTrue之外不必额外设编码SQLite 层面对 UTF-8 是内建支持C# 写入 CSV 时指定编码为Encoding.UTF8并带 BOMnew UTF8Encoding(true)。用 StreamWriter 时别忘掉leaveOpen参数写完记得 Flush。老 zip 里最常见的导出代码是File.WriteAllText(path, content)默认走系统 ANSI 编码到 Excel 必乱这是这类项目第一个要修的通用 bug。5.4 老代码包打不开Framework 版本、依赖 DLL 与 64 位不匹配现象解压 zip双击 .slnVS 提示此项目需要不受支持的 Framework 版本或者编译过了跑起来报未能加载文件或程序集 System.Data.SQLite。原因老 zip 里的项目大多基于 .NET Framework 3.5/4.0 创建而你现在装的可能是 .NET 6/8 SDK 或新版 VS引用的 SQLite 原生 DLL 多是 x86程序集加载在 64 位系统上找错目录。解决先用记事本打开 csproj 看TargetFrameworkVersion如果是 v3.5 或 v4.0直接改成 v4.7.2 或 v4.8多数老代码不需要改语法就能兼容NuGet 重新安装 System.Data.SQLite.Core选最新稳定版注意勾选全部平台删除 bin 和 obj 目录后重新编译如果还报 DLL 加载失败到 NuGet 包目录确认 runtimes 子目录里有没有对应 win-x64 的原生库有就复制到输出目录。坚持用 .NET 6/8 跑老项目不是不行但要先解决 WinForms 设计器兼容和历史 API 移除的问题性价比不如升到 Framework 4.8 来得快。5.5 单号变科学计数法Excel 里 20250612001 成了 2.03E10现象订单号在系统里是字符串导到 Excel 变科学计数法尾号变成 000身份证号、手机号同理。原因Excel 对超过 11 位的纯数字列会自动转数值格式而单号本质是文本被格式拖累。解决源头是设计约束——单号字段定义成TEXT不是INTC# 实体是string不是long。导出到 Excel 的环节要么生成 Excel 文件时显式设置单元格格式为文本要么在单号前拼接一个不可见前缀如或制表符。我的习惯是单号用前缀加日期加序号的结构SO20250612001天然带字母Excel 不会把他当纯数字这类问题从源头就没了。老系统里已经用纯数字单号的迁移时导出 Excel 用 EPPlus 或 NPOI 设置单元格Numberformat 即可。6. 进阶报表打印、CSV 对账与轻量权限控制的落地顺序最小系统跑通、常见坑避完以后最要紧的是把系统从能用推向靠得住。这一章按性价比排序讲三个进阶点——先做 CSV 对账成本低、立刻见效再做报表打印客户感知最强最后加权限内部治理需要并指出每件事的合适规模和实现策略。CSV 对账优先做。给库存表加一个导出全部库存按钮按ProductCode, Name, Quantity, SalePrice, LastInPrice输出再给入库单明细和销售单明细各做一个按日期区间的导出。月底对账就是把这两份 CSV 拉进 Excel 做透视表。实现时按 5.3 节的编码要点用带 BOM 的 UTF-8 写文件空值统一输出为空字符串而不是 null。报表打印看客户既有习惯。客户现在用 Excel 做日报你就别急着上 RDLC 报表客户要求按固定格式打印送货单那就用 RDLC 或 FastReport 类报表组件去套模板。报表模板的维护要交给能改 .rdlc 的人别让只会写 C# 的同事硬扛。权限控制按人头做轻量即可。进销存系统通常就老板、文员、库管三类角色不需要上 RBAC 那套复杂的权限模型。用户表里加一个 Role 字段登录后把角色存进全局静态类在窗体 Load 事件里判断角色控制删除改价红冲这些敏感按钮的可用性。真要细粒度权限再加菜单-角色对照表也不迟但小团队不需要。还有一个所有进阶功能都绕不开的习惯重大操作红冲、删除单据、改库存一律写操作日志日志表至少包含操作人、操作类型、影响单号、时间四个字段。这既不是审计要求也不是负担是出了账目纠纷时你唯一能自证清白的后悔药。我吃过一次亏后来不管项目多小这个日志表永远先建。新系统上线时把初始化库存和初始化客户资料的数据导入脚本写好比在界面上一行行录入顺手得多。用 SQLite 的话写一个seed.sql连商品带库存一起 insert千万注意先插 Product 再插 Stock外键顺序反了就会全部失败。导入后做一次库存合计校验对不上就查 seed 脚本别在界面上慢慢找。这些点做完你的 C# 进销存系统就不再只是一个能录单的界面而是能对账、能交代、能长期维护的工具。我每次交付这类系统都保留一份带注释的建表脚本和连接配置说明换机器部署时照着做两小时就能恢复环境——这些文档和脚本的起点就是当初从那个看似不起眼的 zip 里理出来的思路。希望这篇拆解能帮你少走几段弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑