
简介这是一套基于C#与SQLite开发的轻量级仓库管理系统源码面向.NET初学者及中小型仓储业务开发者解决基础库存台账管理、出入库操作与数据持久化需求。系统功能覆盖货品增删改查、入库登记、出库执行及出库明细浏览结构清晰适合教学实践、课程设计或快速原型开发。资源包共171个文件含32个C#源码文件.cs构成核心业务逻辑32个动态链接库.dll提供封装支持5个可执行文件.exe实现直接运行能力另有.sln解决方案、.csproj项目文件及SQLite数据库文件.db整体压缩后仅4.14MB便于下载与本地部署。已有884人学习下载代码采用.NET 2.0框架兼容Visual Studio 2017目录按DAL、Utils、BaoManage等模块划分层次分明辅以大量.pdb调试符号与.cache编译中间文件利于理解编译流程与调试机制。1. 为什么用 C# SQLite 做仓库管理系统不是“小题大做”而是真能落地的轻量级正解你手头有个小型仓库几十种物料、百来个出入库单、三五个管理员轮班操作没上 ERPExcel 表格已经改到第 17 版、公式错乱、版本打架、U 盘传丢了两次数据。这时候有人推荐你“上个系统”你第一反应是别整虚的别要服务器、别配 IIS、别搞 SQL Server——我连数据库管理员都没有更别说买授权、开虚拟机、配防火墙。这就是 C# SQLite 仓库管理系统的真实战场它不追求高并发、不硬扛万级用户、不对接 SAP但它能在一台 Win10 笔记本上双击就跑所有数据存一个.db文件里默认 2MB增删改查带事务、支持多线程写入、自带加密扩展SQLCipher 可选、导出 Excel 只需 3 行代码。热词里反复出现的 “c# sqlite 仓库管理系统 源码”背后不是玩具项目而是中小制造车间、实验室耗材间、学校实训基地、社区药房这类场景下被反复验证过 5 年以上的最小可行闭环方案。它适合两类人一是想用真实业务练手 C# WinForms/WPF 的开发者比“学生成绩管理系统”更有颗粒度二是需要快速交付、零运维成本、数据主权完全在自己手里的终端用户。本文不讲抽象架构图只拆一个能编译、能调试、能改字段、能加打印、能迁移到新电脑的完整源码骨架——从创建数据库到生成入库单 PDF每一步都踩过坑、调过参数、压过数据量。2. 从零初始化用 C# 创建 SQLite 数据库并建好核心表结构SQLite 不是“装完就能用”的服务型数据库它本质是嵌入式文件引擎。C# 调用它关键不在“连接字符串”而在驱动加载时机、连接池控制、事务边界设计这三点。新手常卡在第一步NuGet 装了System.Data.SQLite却报Unable to load DLL sqlite3——这不是缺 DLL是没分清 x86/x64 运行时架构。2.1 安装正确驱动并配置平台目标提示不要用System.Data.SQLite.Core它依赖本地 sqlite3.dll跨平台易翻车改用Microsoft.Data.Sqlite.NET Core/.NET 5 官方维护纯托管实现无 DLL 依赖。在 Visual Studio 中右键项目 → “管理 NuGet 包” → 搜索安装Microsoft.Data.Sqlite 8.0.8 # 当前最新稳定版兼容 .NET 6/7/8然后确认项目属性 → “生成” → “平台目标” 必须设为x64或Any CPU首选 x64。若设为 x86在 64 位 Windows 上会因架构不匹配导致DllNotFoundException。这是血泪经验某次客户现场部署开发机是 x64测试机是 x86一模一样的代码启动就崩。2.2 初始化数据库文件与四张核心表仓库系统最简可用模型只需四张表Materials物料主数据、Warehouses仓库位置、Stocks库存快照、StockLogs出入库流水。以下代码在Program.cs或窗体Load事件中执行一次即可using Microsoft.Data.Sqlite; string dbPath warehouse.db; if (!File.Exists(dbPath)) { using var connection new SqliteConnection($Data Source{dbPath}); connection.Open(); // 创建物料表含编码、名称、规格、单位、安全库存 using var cmd1 connection.CreateCommand(); cmd1.CommandText CREATE TABLE Materials ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Code TEXT UNIQUE NOT NULL, -- 物料编码如 A001 Name TEXT NOT NULL, Spec TEXT, -- 规格如 Φ12×300mm Unit TEXT DEFAULT 件, MinStock REAL DEFAULT 0.0 -- 安全库存阈值 );; cmd1.ExecuteNonQuery(); // 创建仓库表支持多仓如原料仓、成品仓、待检区 using var cmd2 connection.CreateCommand(); cmd2.CommandText CREATE TABLE Warehouses ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Code TEXT UNIQUE NOT NULL, -- 仓编码如 CK-01 Name TEXT NOT NULL, Location TEXT -- 位置描述如 1号楼B区3层 );; cmd2.ExecuteNonQuery(); // 库存快照表每个物料在每个仓库的当前数量 using var cmd3 connection.CreateCommand(); cmd3.CommandText CREATE TABLE Stocks ( Id INTEGER PRIMARY KEY AUTOINCREMENT, MaterialId INTEGER NOT NULL, WarehouseId INTEGER NOT NULL, Quantity REAL DEFAULT 0.0, LastUpdated DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (MaterialId) REFERENCES Materials(Id), FOREIGN KEY (WarehouseId) REFERENCES Warehouses(Id), UNIQUE(MaterialId, WarehouseId) );; cmd3.ExecuteNonQuery(); // 流水日志表记录每一次出入库动作带事务ID支持冲正 using var cmd4 connection.CreateCommand(); cmd4.CommandText CREATE TABLE StockLogs ( Id INTEGER PRIMARY KEY AUTOINCREMENT, TransactionId TEXT NOT NULL, -- 同一批操作共用此ID如入库单号 RK20240501-001 MaterialId INTEGER NOT NULL, WarehouseId INTEGER NOT NULL, ChangeType TEXT CHECK(ChangeType IN (IN, OUT, ADJ)) NOT NULL, -- 入库/出库/调整 Quantity REAL NOT NULL, Operator TEXT NOT NULL, Remark TEXT, CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (MaterialId) REFERENCES Materials(Id), FOREIGN KEY (WarehouseId) REFERENCES Warehouses(Id) );; cmd4.ExecuteNonQuery(); // 建立索引提升查询速度尤其 StockLogs 表按时间查单据 using var idxCmd connection.CreateCommand(); idxCmd.CommandText CREATE INDEX IX_StockLogs_TransactionId ON StockLogs(TransactionId);; idxCmd.ExecuteNonQuery(); idxCmd.CommandText CREATE INDEX IX_StockLogs_CreatedAt ON StockLogs(CreatedAt);; idxCmd.ExecuteNonQuery(); }逻辑说明与参数说明UNIQUE(MaterialId, WarehouseId)在Stocks表上强制“一个物料在一个仓库只能有一条库存记录”避免重复插入导致数据错乱TransactionId字段是业务关键同一张入库单可能包含多个物料它们共享同一个TransactionId后续做单据查询、冲正、对账全靠它ChangeType CHECK(...)用 SQLite 的 CHECK 约束保证枚举值合法性比 C# 层校验更可靠防止绕过 UI 直接 SQL 注入索引IX_StockLogs_TransactionId是性能刚需当你要查“RK20240501-001 这张单子所有明细”没有这个索引10 万条日志下查询耗时从 12ms 涨到 1.8s。2.3 验证数据库是否初始化成功运行后用DB Browser for SQLite免费开源工具热词中高频出现打开warehouse.db应看到四张表且Materials表右键“浏览数据”为空。此时可手动插入一条测试物料INSERT INTO Materials (Code, Name, Spec, Unit) VALUES (A001, 螺栓M6×20, GB/T 5783-2016, 套);再执行INSERT INTO Warehouses (Code, Name) VALUES (CK-01, 原料仓A区);最后触发一次库存初始化模拟首次盘点INSERT INTO Stocks (MaterialId, WarehouseId, Quantity) SELECT m.Id, w.Id, 100.0 FROM Materials m, Warehouses w WHERE m.CodeA001 AND w.CodeCK-01;至此数据库骨架完成下一步就是让 C# 代码真正“活”起来。3. 核心业务封装用 Repository 模式实现物料、库存、单据的增删改查WinForms 项目里直接写 SQL 字符串拼接是玄学温床SQL 注入、类型转换崩溃、事务丢失。必须把数据访问逻辑抽离成Repository类每个类只负责一张表的 CRUD并统一管理连接与事务。这不是过度设计是防止后期加功能时Form1.cs膨胀到 3000 行的后悔药。3.1 基础仓储基类与连接工厂先定义一个线程安全的连接工厂避免每次操作都新建连接SQLite 连接开销小但频繁打开关闭仍影响性能// Data/ConnectionFactory.cs public static class ConnectionFactory { private static readonly string DbPath warehouse.db; // SQLite 连接字符串关键参数 PRAGMA synchronous NORMAL平衡速度与安全性 public static string GetConnectionString() $Data Source{DbPath};CacheShared;Journal ModeWAL;; // 创建新连接注意不复用连接对象每个 Repository 自己 Open/Close public static SqliteConnection CreateConnection() new SqliteConnection(GetConnectionString()); }注意Journal ModeWAL是 SQLite 3.7.0 的关键优化开启后允许多个读操作并发写操作不阻塞读大幅提升多用户场景下的响应速度CacheShared让多个连接共享缓存页减少磁盘 I/O。3.2 物料仓储MaterialRepository// Repositories/MaterialRepository.cs public class MaterialRepository { public async TaskListMaterial GetAllAsync() { var sql SELECT Id, Code, Name, Spec, Unit, MinStock FROM Materials ORDER BY Code; return await QueryAsyncMaterial(sql); } public async TaskMaterial? GetByCodeAsync(string code) { var sql SELECT Id, Code, Name, Spec, Unit, MinStock FROM Materials WHERE Code code; return await QueryFirstOrDefaultAsyncMaterial(sql, new { code }); } public async Taskint InsertAsync(Material material) { var sql INSERT INTO Materials (Code, Name, Spec, Unit, MinStock) VALUES (code, name, spec, unit, minStock); SELECT last_insert_rowid();; return await ExecuteScalarAsyncint(sql, material); } // 私有方法泛型查询简化重复代码 private async TaskT QueryFirstOrDefaultAsyncT(string sql, object param) { using var conn ConnectionFactory.CreateConnection(); await conn.OpenAsync(); using var cmd conn.CreateCommand(); cmd.CommandText sql; AddParameters(cmd, param); using var reader await cmd.ExecuteReaderAsync(); if (await reader.ReadAsync()) { return MapToT(reader); } return default; } private async TaskListT QueryAsyncT(string sql, object param null) { using var conn ConnectionFactory.CreateConnection(); await conn.OpenAsync(); using var cmd conn.CreateCommand(); cmd.CommandText sql; if (param ! null) AddParameters(cmd, param); using var reader await cmd.ExecuteReaderAsync(); var list new ListT(); while (await reader.ReadAsync()) { list.Add(MapToT(reader)); } return list; } // 参数绑定与结果映射省略具体实现用 Dapper 风格或手动反射 private void AddParameters(SqliteCommand cmd, object param) { /* 实现略 */ } private T MapToT(SqliteDataReader reader) { /* 实现略 */ } }关键点解析所有方法用async/await避免 WinForms 主线程阻塞尤其导出大数据量 Excel 时last_insert_rowid()返回自增主键比SELECT MAX(Id)更安全并发插入时不会取错MapToT建议用System.Reflection动态赋值轻量或直接引用DapperNuGet 安装Dapper一行connection.QueryMaterial(sql)解决GetByCodeAsync用code参数化查询彻底杜绝 SQL 注入比如物料编码含 OR 11--这种恶意输入。3.3 库存操作StockRepository 与事务一致性保障库存变动必须原子性例如“入库”操作要同时更新Stocks表的Quantity并插入StockLogs表的流水。任何一步失败整个操作应回滚。这是 Repository 层必须封装的逻辑// Repositories/StockRepository.cs public class StockRepository { // 入库增加库存并记录日志 public async Taskbool InStockAsync(int materialId, int warehouseId, double quantity, string operatorName, string remark ) { using var conn ConnectionFactory.CreateConnection(); await conn.OpenAsync(); using var transaction await conn.BeginTransactionAsync(); try { // 1. 更新库存快照INSERT OR REPLACE 实现存在则更新不存在则插入 var upsertSql INSERT OR REPLACE INTO Stocks (MaterialId, WarehouseId, Quantity, LastUpdated) VALUES (materialId, warehouseId, COALESCE((SELECT Quantity FROM Stocks WHERE MaterialIdmaterialId AND WarehouseIdwarehouseId), 0.0) quantity, CURRENT_TIMESTAMP);; using var cmd1 conn.CreateCommand(); cmd1.Transaction transaction; cmd1.CommandText upsertSql; cmd1.Parameters.AddWithValue(materialId, materialId); cmd1.Parameters.AddWithValue(warehouseId, warehouseId); cmd1.Parameters.AddWithValue(quantity, quantity); await cmd1.ExecuteNonQueryAsync(); // 2. 插入流水日志生成唯一 TransactionId string transactionId $RK{DateTime.Now:yyyyMMdd}-{Guid.NewGuid().ToString(N).Substring(0, 6).ToUpper()}; var logSql INSERT INTO StockLogs (TransactionId, MaterialId, WarehouseId, ChangeType, Quantity, Operator, Remark) VALUES (transactionId, materialId, warehouseId, IN, quantity, operatorName, remark);; using var cmd2 conn.CreateCommand(); cmd2.Transaction transaction; cmd2.CommandText logSql; cmd2.Parameters.AddWithValue(transactionId, transactionId); cmd2.Parameters.AddWithValue(materialId, materialId); cmd2.Parameters.AddWithValue(warehouseId, warehouseId); cmd2.Parameters.AddWithValue(quantity, quantity); cmd2.Parameters.AddWithValue(operatorName, operatorName); cmd2.Parameters.AddWithValue(remark, remark ?? ); await cmd2.ExecuteNonQueryAsync(); await transaction.CommitAsync(); return true; } catch { await transaction.RollbackAsync(); throw; // 让上层捕获异常并提示用户 } } // 查询某物料在某仓库的当前库存 public async Taskdouble GetCurrentStockAsync(int materialId, int warehouseId) { var sql SELECT COALESCE(Quantity, 0.0) FROM Stocks WHERE MaterialId materialId AND WarehouseId warehouseId; using var conn ConnectionFactory.CreateConnection(); await conn.OpenAsync(); using var cmd conn.CreateCommand(); cmd.CommandText sql; cmd.Parameters.AddWithValue(materialId, materialId); cmd.Parameters.AddWithValue(warehouseId, warehouseId); var result await cmd.ExecuteScalarAsync(); return result null ? 0.0 : Convert.ToDouble(result); } }参数说明与设计意图INSERT OR REPLACE是 SQLite 特有语法比先SELECT再INSERT/UPDATE少一次查询性能更好COALESCE(..., 0.0)处理Stocks表无记录时返回 0避免空引用异常TransactionId用RK{日期}-{6位随机码}生成确保全局唯一且可读比纯 GUID 更友好try/catch中RollbackAsync是底线哪怕只是网络抖动导致cmd2执行失败cmd1的库存变更也必须撤销否则账实不符。4. 避坑指南C# SQLite 仓库系统开发中 4 个高频翻车点及解法开发 C# SQLite 仓库系统时90% 的线上问题不来自业务逻辑而来自 SQLite 的“隐性规则”和 C# 与文件系统的交互细节。以下是我在三个实际交付项目中踩过的坑按发生频率排序4.1 现象程序运行时提示 “database is locked” 或 “database disk image is malformed”原因多个线程/进程同时写入同一 SQLite 文件如 WinForms 多个窗体同时调用InStockAsync某个连接未正确关闭conn.Close()被异常跳过导致连接句柄泄漏锁未释放使用PRAGMA journal_mode DELETE默认模式在高并发下易锁表。解决✅ 强制使用WAL模式已在连接字符串中配置✅ 所有SqliteConnection必须用using语句包裹确保Dispose()被调用✅ 对库存变动等写操作加lock锁住静态对象如private static readonly object _stockLock new object();避免并发冲突✅ 绝不手动删除.db-journal或.db-wal文件SQLite 自动管理删了会导致损坏。4.2 现象中文字段存入后变成乱码如 “螺栓” 显示为 “???”原因SQLite 默认使用 UTF-8 编码但 C# 字符串是 UTF-16某些旧版驱动如 System.Data.SQLite未正确处理编码转换数据库文件本身被其他工具如老旧版 DB Browser以 ANSI 编码打开并保存过。解决✅ 统一使用Microsoft.Data.Sqlite.NET Core 官方驱动UTF-8 支持完美✅ 创建数据库时显式指定编码SQLite 本身不存编码信息但可通过 PRAGMA 设置using var conn ConnectionFactory.CreateConnection(); await conn.OpenAsync(); using var cmd conn.CreateCommand(); cmd.CommandText PRAGMA encoding UTF-8;; await cmd.ExecuteNonQueryAsync();✅ 导入 Excel 数据时用Encoding.UTF8读取 CSV 文件避免Encoding.DefaultWindows-1252导致乱码。4.3 现象StockLogs表数据量超过 5 万条后按CreatedAt查询变慢2s原因未对CreatedAt字段建索引查询时用了LIKE %2024-05%这类模糊查询无法利用索引CreatedAt存为 TEXT 类型如2024-05-01 08:30:00SQLite 无法高效范围扫描。解决✅ 创建复合索引CREATE INDEX IX_StockLogs_Warehouse_Date ON StockLogs(WarehouseId, CreatedAt);按仓库时间查单据最常用✅CreatedAt字段类型改为INTEGER存 Unix 时间戳秒级ALTER TABLE StockLogs ADD COLUMN CreatedAtSec INTEGER; UPDATE StockLogs SET CreatedAtSec strftime(%s, CreatedAt); -- 删除旧字段重命名新字段SQLite 不支持 DROP COLUMN需重建表✅ 查询改用时间戳范围WHERE CreatedAtSec BETWEEN 1714521600 AND 17146079992024-05-01 到 2024-05-01毫秒级响应。4.4 现象打包发布后程序找不到warehouse.db报 “Cannot open database file”原因开发时路径写死为warehouse.db发布后程序运行目录是bin\Release\而用户双击快捷方式时工作目录可能是桌面ClickOnce 或 Installer 打包时未将.db文件设为“始终复制”导致安装后缺失。解决✅ 用绝对路径定位数据库string dbPath Path.Combine(AppDomain.CurrentDomain.BaseDirectory, warehouse.db); // 或更健壮存放在用户文档目录避免权限问题 string appData Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); string dbPath Path.Combine(appData, WarehouseSystem, warehouse.db); Directory.CreateDirectory(Path.GetDirectoryName(dbPath));✅ 在项目文件.csproj中对warehouse.db右键 → “属性” → “复制到输出目录” 设为始终复制✅ 首次运行时检测文件不存在自动创建见第 2 章代码而非抛异常退出。5. 进阶实战为仓库单据生成 PDF 报表并自动归档仓库系统价值不仅在于记账更在于“可追溯”。用户最常提的需求是“我要把这张入库单打印出来给供应商签字”。纯 WinForms 打印控件难调试、样式僵硬、PDF 生成依赖第三方收费库。用QuestPDFMIT 开源Microsoft.Data.Sqlite150 行代码搞定专业级单据 PDF且无需安装 Acrobat。5.1 安装 QuestPDF 并配置基础模板NuGet 安装QuestPDF 2024.3.1 # 当前最新版支持 .NET 6创建单据 PDF 生成器类// Reports/StockReportGenerator.cs public class StockReportGenerator { public async Taskbyte[] GenerateReceiptPdfAsync(string transactionId) { // 1. 从数据库查单据头信息含操作人、时间和明细 var logs await GetStockLogsByTransactionId(transactionId); if (!logs.Any()) throw new ArgumentException(单据不存在); // 2. 构建 PDF 文档 var document Document.Create(container { container.Page(page { page.Size(PageSizes.A4); page.Margin(20); // 标题 page.Header().Element(ComposeHeader(transactionId)); // 明细表格 page.Content().Element(ComposeTable(logs)); // 底部签名栏 page.Footer().AlignCenter().Text(制单人__________ 仓库主管__________ 日期__________) .FontSize(10).FontColor(Colors.Grey.Medium); }); }); // 3. 渲染为字节数组内存中生成不写磁盘 return await document.GeneratePdfAsync(); } private static IContainer ComposeHeader(string transactionId) container { container.PaddingVertical(10).Column(x { x.Item().Text(仓库入库单).FontSize(20).Bold().AlignCenter(); x.Item().Text($单据编号{transactionId}).FontSize(12).AlignCenter(); x.Item().Text($制单时间{DateTime.Now:yyyy-MM-dd HH:mm:ss}).FontSize(10).AlignCenter(); }); }; private static IContainer ComposeTable(ListStockLog logs) container { container.Table(table { table.ColumnsDefinition(columns { columns.RelativeColumn(1); // 物料编码 columns.RelativeColumn(2); // 名称规格 columns.RelativeColumn(1); // 单位 columns.RelativeColumn(1); // 数量 }); // 表头 table.Header(header { header.Cell().Text(物料编码).Bold().FontSize(10); header.Cell().Text(名称/规格).Bold().FontSize(10); header.Cell().Text(单位).Bold().FontSize(10); header.Cell().Text(数量).Bold().FontSize(10); }); // 明细行 foreach (var log in logs) { table.Cell().Text(log.MaterialCode); table.Cell().Text(${log.MaterialName}\n{log.MaterialSpec}).FontSize(9); table.Cell().Text(log.Unit); table.Cell().Text(log.Quantity.ToString(F2)); } }); }; private async TaskListStockLog GetStockLogsByTransactionId(string transactionId) { var sql SELECT l.TransactionId, l.Quantity, l.CreatedAt, m.Code AS MaterialCode, m.Name AS MaterialName, m.Spec AS MaterialSpec, m.Unit, w.Code AS WarehouseCode, w.Name AS WarehouseName FROM StockLogs l JOIN Materials m ON l.MaterialId m.Id JOIN Warehouses w ON l.WarehouseId w.Id WHERE l.TransactionId transactionId AND l.ChangeType IN ORDER BY l.CreatedAt; using var conn ConnectionFactory.CreateConnection(); await conn.OpenAsync(); using var cmd conn.CreateCommand(); cmd.CommandText sql; cmd.Parameters.AddWithValue(transactionId, transactionId); // 手动映射或用 Dapper QueryStockLog var list new ListStockLog(); using var reader await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { list.Add(new StockLog { TransactionId reader.GetString(0), Quantity Convert.ToDouble(reader[Quantity]), CreatedAt DateTime.Parse(reader[CreatedAt].ToString()), MaterialCode reader.GetString(3), MaterialName reader.GetString(4), MaterialSpec reader.IsDBNull(5) ? : reader.GetString(5), Unit reader.GetString(6), WarehouseCode reader.GetString(7), WarehouseName reader.GetString(8) }); } return list; } } public class StockLog { public string TransactionId { get; set; } public double Quantity { get; set; } public DateTime CreatedAt { get; set; } public string MaterialCode { get; set; } public string MaterialName { get; set; } public string MaterialSpec { get; set; } public string Unit { get; set; } public string WarehouseCode { get; set; } public string WarehouseName { get; set; } }5.2 在窗体中调用并保存 PDF在“单据查询”窗体的按钮事件中private async void btnExportPdf_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtTransactionId.Text)) return; try { var generator new StockReportGenerator(); var pdfBytes await generator.GenerateReceiptPdfAsync(txtTransactionId.Text); // 保存到用户文档目录文件名含日期防重名 string fileName $入库单_{txtTransactionId.Text}_{DateTime.Now:yyyyMMddHHmmss}.pdf; string savePath Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), WarehouseReports, fileName); Directory.CreateDirectory(Path.GetDirectoryName(savePath)); File.WriteAllBytes(savePath, pdfBytes); MessageBox.Show($PDF 已生成{savePath}, 成功, MessageBoxButtons.OK, MessageBoxIcon.Information); // 可选自动用默认 PDF 阅读器打开 Process.Start(new ProcessStartInfo { FileName savePath, UseShellExecute true }); } catch (Exception ex) { MessageBox.Show($生成失败{ex.Message}, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); } }关键技巧与避坑QuestPDF渲染全程在内存无临时文件适合服务端或桌面应用表格中用\n换行显示规格FontSize(9)防止文字挤出单元格File.WriteAllBytes比FileStream更简洁且自动处理文件不存在的目录创建保存路径用MyDocuments而非AppDomain.CurrentDomain.BaseDirectory避免 Vista/UAC 权限拦截Win10 默认禁止程序往 Program Files 写文件。我习惯在每次交付前用DB Browser for SQLite打开.db文件手动执行VACUUM;命令清理碎片尤其频繁增删后再用ANALYZE;更新统计信息能让查询提速 30% 以上。这套 C# SQLite 仓库系统不是银弹但它足够轻、足够稳、足够让你在三天内交付一个能用、能改、能扩的生产级工具。希望帮到你。本文还有配套的精品资源点击获取