news 2026/10/7 21:42:25

C#台账记录系统源码实战:SQLite存储、并发写入与查询导出优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#台账记录系统源码实战:SQLite存储、并发写入与查询导出优化

简介:这是一套面向C#初学者与中小型企业管理软件开发者的台账记录系统设计源码,聚焦组织或企业日常台账的录入、查询、更新与删除等核心业务场景,适合作为课程设计、毕业设计或二次开发的参考模板。压缩包共67个文件、约384KB,其中41个C#源代码文件承载主要业务逻辑,11个resx资源文件与3个png、2个ico负责界面与图标资源,另有3个csproj项目文件、2个sln解决方案、2个settings设置文件、1个config配置文件和1个db3数据库文件,完整覆盖从项目配置到数据持久化的各个环节。目录中可见AccountLog、CheckPay、ConsoleApplication1等多个子项目,以及Form系列窗体、UserCtrl自定义控件、sqlUtil数据库工具类与Services服务层,模块划分清晰,并运用了工厂、单例、策略等设计模式,便于理解分层架构与扩展维护。目前已有351人学习下载,开发者可据此快速掌握C#桌面台账系统的整体结构与实现思路。

1. 台账记录系统为什么总在“能用”和“好用”之间翻车

很多做 C# 上位机或者内部管理工具的兄弟,接到“台账记录系统”这个需求时,第一反应往往是:不就是增删改查吗?建个表、拖几个控件、写个INSERT就交差了。但真到车间里跑两周,问题全冒出来了:操作员说查上个月的记录卡到转圈,财务说导出的 Excel 对不上号,最要命的是某天早上打开软件,发现昨天的数据全没了——因为程序还在用DataTable往内存里怼,压根没落盘。

台账记录系统的核心不是“记录”,而是“台账”两个字背后的东西:可追溯、可审计、可汇总。它跟普通的日志系统不一样,日志可以丢几条,台账丢一条就是事故。所以基于 C# 语言做这套源码,重点不在界面多花哨,而在数据怎么存、并发怎么控、查询怎么快、导出怎么稳。这篇笔记面向的是手里有 Visual Studio、懂一点 C# 基础,但没想清楚台账系统架构边界的开发者。我会按实际落地的顺序,把表结构、数据访问层、并发处理、查询优化和导出这几个环节拆开讲,中间穿插我踩过的坑和调参经验。看完你至少能判断:手里的项目是该继续用 Access 顶着,还是趁早换 SQLite 或 SQL Server Express。

2. 先定存储引擎再写代码:Access、SQLite、SQL Server Express 怎么选

2.1 三种存储方案的硬指标对比

选存储引擎这件事,很多教程一笔带过,结果新手拿着 Access 做多用户并发,直接把自己埋了。我把三种常见方案在台账场景下的表现列出来,你对着自己的工位环境挑。

对比项Access (.accdb)SQLiteSQL Server Express
并发写入极差,锁文件较好,WAL 模式可并发读好,行级锁
单文件部署是是否,需装服务
最大库大小2GB 左右理论 281TB,实际看磁盘10GB
网络共享勉强,易损坏不推荐原生支持
C# 驱动OleDbSystem.Data.SQLite / Microsoft.Data.SqliteSqlClient
适合场景单机单用户单机多用户/边缘设备局域网多客户端

如果你的台账系统就装在一台工控机上,操作员轮流用,SQLite 是性价比最高的选择。如果车间里三台电脑都要连同一个库,别犹豫,上 SQL Server Express。Access 只适合做原型验证,千万别拿去跑生产。

2.2 用 C# 建库建表的最小可跑代码

选定 SQLite 后,第一步不是拖控件,而是把表结构用代码固化下来。我一般会在程序启动时检查库文件是否存在,不存在就建表。下面这段代码可以直接抄进Program.cs或者一个专门的DbInitializer类。

using Microsoft.Data.Sqlite; using System.IO; public static class DbInitializer { private static string dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ledger.db"); private static string connStr = $"Data Source={dbPath}"; public static void EnsureCreated() { bool isNew = !File.Exists(dbPath); using var conn = new SqliteConnection(connStr); conn.Open(); // 开启 WAL 模式,提升并发读性能 using (var pragma = conn.CreateCommand()) { pragma.CommandText = "PRAGMA journal_mode=WAL;"; pragma.ExecuteNonQuery(); } if (isNew) { using var cmd = conn.CreateCommand(); cmd.CommandText = @" CREATE TABLE Ledger ( Id INTEGER PRIMARY KEY AUTOINCREMENT, RecordTime TEXT NOT NULL, Category TEXT NOT NULL, ItemName TEXT NOT NULL, Quantity REAL NOT NULL DEFAULT 0, Operator TEXT NOT NULL, Remark TEXT, CreatedAt TEXT NOT NULL DEFAULT (datetime('now','localtime')) ); CREATE INDEX idx_ledger_time ON Ledger(RecordTime); CREATE INDEX idx_ledger_category ON Ledger(Category); "; cmd.ExecuteNonQuery(); } } }

这段代码的逻辑很直白:先判断文件在不在,不在才建表,避免每次启动重复执行 DDL。PRAGMA journal_mode=WAL是 SQLite 的写前日志模式,开了之后读操作不会被写操作阻塞,台账系统里查询频率远高于写入,这个参数必开。两个索引分别建在RecordTime和Category上,因为台账查询九成是按时间范围或者按类别筛选。注意RecordTime我用了 TEXT 类型存 ISO8601 字符串,SQLite 没有原生日期类型,用 TEXT 排序和比较反而最稳,别用datetime函数去存,时区能把你搞疯。

2.3 连接字符串里的三个关键参数

SQLite 的连接字符串看着简单,但有几个参数不设,性能差一倍。我习惯写成这样:

string connStr = "Data Source=ledger.db;Cache=Shared;Pooling=True;";

Cache=Shared让多个连接共享页缓存,减少内存占用;Pooling=True开启连接池,避免频繁开关连接。还有一个Default Timeout参数,默认是 30 秒,如果台账写入量大,可以调到 60,防止database is locked异常。这些参数在 Microsoft.Data.Sqlite 里都支持,别用那个老掉牙的 System.Data.SQLite,两者 API 不兼容,混用会报奇怪的错。

3. 数据访问层:手写 ADO.NET 还是上 Entity Framework Core

3.1 两种方案的取舍依据

台账系统的数据访问层,我见过两种极端:一种是全程SqlDataAdapter加DataTable,另一种是无脑上 EF Core 然后抱怨慢。我的经验是,台账这种表结构稳定、查询模式固定的场景,手写 ADO.NET 反而更可控。EF Core 的优势在快速建模和迁移,但它的 LINQ 查询在复杂汇总时生成的 SQL 经常出人意料,调优成本高。

具体怎么选:如果台账字段不超过 20 个,查询以简单条件筛选为主,手写 ADO.NET 加 Dapper 是最佳组合。Dapper 是 StackExchange 开源的微型 ORM,只做对象映射,SQL 还是你自己写,性能接近原生。如果台账需要跟其他业务表关联,字段经常变,那就上 EF Core,但记得用AsNoTracking()做只读查询。

3.2 用 Dapper 封装台账的增删改查

先通过 NuGet 装Dapper和Microsoft.Data.Sqlite,然后定义一个实体类和仓储类。

public class LedgerRecord { public long Id { get; set; } public string RecordTime { get; set; } public string Category { get; set; } public string ItemName { get; set; } public double Quantity { get; set; } public string Operator { get; set; } public string Remark { get; set; } } public class LedgerRepository { private readonly string _connStr; public LedgerRepository(string connStr) => _connStr = connStr; public int Insert(LedgerRecord record) { using var conn = new SqliteConnection(_connStr); string sql = @"INSERT INTO Ledger (RecordTime, Category, ItemName, Quantity, Operator, Remark) VALUES (@RecordTime, @Category, @ItemName, @Quantity, @Operator, @Remark)"; return conn.Execute(sql, record); } public IEnumerable<LedgerRecord> QueryByDateRange(string start, string end) { using var conn = new SqliteConnection(_connStr); string sql = @"SELECT * FROM Ledger WHERE RecordTime >= @start AND RecordTime <= @end ORDER BY RecordTime DESC"; return conn.Query<LedgerRecord>(sql, new { start, end }); } }

Insert方法里 Dapper 自动把实体属性映射到 SQL 参数,省去手写AddWithValue的繁琐。QueryByDateRange用参数化查询防止 SQL 注入,同时因为RecordTime上有索引,范围查询走索引扫描,几千条数据毫秒级返回。注意RecordTime传参时格式要统一成yyyy-MM-dd HH:mm:ss,否则字符串比较会出错。我一般在界面层就把时间格式化成这个格式再传进来。

3.3 批量插入的坑与优化

台账系统经常需要从 Excel 导入历史数据,一条条Insert能慢到让你怀疑人生。SQLite 默认每条 INSERT 都是一个独立事务,磁盘 I/O 次数等于记录数。解决办法是用显式事务包起来。

public void BatchInsert(IEnumerable<LedgerRecord> records) { using var conn = new SqliteConnection(_connStr); conn.Open(); using var tran = conn.BeginTransaction(); string sql = @"INSERT INTO Ledger (RecordTime, Category, ItemName, Quantity, Operator, Remark) VALUES (@RecordTime, @Category, @ItemName, @Quantity, @Operator, @Remark)"; foreach (var r in records) { conn.Execute(sql, r, tran); } tran.Commit(); }

加了事务之后,一万条插入从十几秒降到一秒以内。这里有个细节:conn.Execute的第三个参数传tran,Dapper 会自动把命令挂到事务上。如果你用SqlBulkCopy,那是 SQL Server 的专属,SQLite 没有对应实现,别去搜了。

4. 并发写入与界面卡顿:台账系统最容易翻车的两个点

4.1 多用户同时写台账时的锁冲突处理

SQLite 在 WAL 模式下支持一个写多个读,但多个写之间还是互斥的。如果两个操作员同时点保存,后到的那个会收到SQLITE_BUSY异常。我见过有人直接try-catch吞掉异常,结果数据丢了都不知道。正确做法是设置忙等待超时。

string connStr = "Data Source=ledger.db;Cache=Shared;Pooling=True;Default Timeout=30;";

Default Timeout=30让 SQLite 在遇到锁时自动重试 30 秒,而不是立刻抛异常。30 秒对台账写入来说足够了,如果还超时,说明有长事务没提交,得去查代码里是不是有BeginTransaction之后忘了Commit。另外,写操作尽量短平快,别在事务里做界面刷新或者文件 IO。

4.2 用 async/await 把查询从 UI 线程剥离

WinForms 或 WPF 里直接在按钮点击事件里跑查询,数据量一大界面就假死。操作员以为死机了,狂点按钮,结果触发多次查询,雪上加霜。解决办法是把数据访问改成异步。

public async Task<List<LedgerRecord>> QueryByDateRangeAsync(string start, string end) { using var conn = new SqliteConnection(_connStr); string sql = @"SELECT * FROM Ledger WHERE RecordTime >= @start AND RecordTime <= @end ORDER BY RecordTime DESC"; var result = await conn.QueryAsync<LedgerRecord>(sql, new { start, end }); return result.ToList(); }

按钮事件里用await调用,UI 线程不会被阻塞。注意Microsoft.Data.Sqlite的异步方法底层其实是同步的,但QueryAsync会把执行放到线程池,对界面响应来说效果一样。如果你追求真异步,可以换SQLitePCLRaw的异步 API,但复杂度高,台账场景没必要。

4.3 写入频率高时的队列缓冲策略

有些台账场景是设备自动上报,每秒好几条。这种频率下每条都开连接写库,连接池也扛不住。我一般加一个内存队列,后台线程每 500 毫秒批量刷一次。

private ConcurrentQueue<LedgerRecord> _queue = new(); private Timer _flushTimer; public void Enqueue(LedgerRecord record) => _queue.Enqueue(record); private void FlushTimer_Elapsed(object sender, ElapsedEventArgs e) { var batch = new List<LedgerRecord>(); while (_queue.TryDequeue(out var r) && batch.Count < 100) batch.Add(r); if (batch.Count > 0) _repository.BatchInsert(batch); }

ConcurrentQueue保证多线程入队安全,定时器每 500 毫秒取最多 100 条批量写。这样即使设备上报再快,数据库压力也是恒定的。注意程序退出时要手动调一次Flush,否则队列里剩的数据就丢了,这是血泪教训。

5. 台账查询与导出的避坑清单

5.1 时间范围查询为什么慢:索引失效的三种写法

现象:台账数据到五万条以后,按时间范围查询要等五六秒。原因:SQL 写法导致索引失效。解决:检查你的 WHERE 子句。

第一种坑:WHERE strftime('%Y-%m', RecordTime) = '2024-05'。对字段用函数,索引直接废掉。改成WHERE RecordTime >= '2024-05-01' AND RecordTime < '2024-06-01'。

第二种坑:WHERE RecordTime LIKE '2024-05%'。LIKE 以通配符结尾时 SQLite 能走索引,但以%开头就不行。台账查询尽量用范围比较,别用 LIKE。

第三种坑:参数类型不匹配。RecordTime存的是 TEXT,你传个DateTime对象进去,驱动会做隐式转换,索引同样失效。统一传格式化好的字符串。

5.2 导出 Excel 时内存暴涨怎么破

现象:导出三万条台账到 Excel,程序内存从 100MB 飙到 2GB,最后OutOfMemoryException。原因:用了Microsoft.Office.Interop.Excel,每写一个单元格就跨进程调用一次 COM,内存全堆在 Excel 进程里。解决:换ClosedXML或者EPPlus,纯 .NET 实现,流式写入。

using ClosedXML.Excel; public void ExportToExcel(List<LedgerRecord> records, string filePath) { using var workbook = new XLWorkbook(); var ws = workbook.Worksheets.Add("台账"); ws.Cell(1, 1).Value = "记录时间"; ws.Cell(1, 2).Value = "类别"; ws.Cell(1, 3).Value = "品名"; ws.Cell(1, 4).Value = "数量"; ws.Cell(1, 5).Value = "操作员"; for (int i = 0; i < records.Count; i++) { var r = records[i]; ws.Cell(i + 2, 1).Value = r.RecordTime; ws.Cell(i + 2, 2).Value = r.Category; ws.Cell(i + 2, 3).Value = r.ItemName; ws.Cell(i + 2, 4).Value = r.Quantity; ws.Cell(i + 2, 5).Value = r.Operator; } workbook.SaveAs(filePath); }

ClosedXML在内存里构建文档树,最后一次性写盘,三万条大概占 200MB 内存,可接受。如果数据量再大,用EPPlus的SaveAs流式模式,或者直接导出 CSV,别跟 Excel 死磕。

5.3 台账数据被误删后的恢复手段

现象:操作员误点了“清空本月数据”,或者程序 bug 导致DELETE没加WHERE。原因:没有软删除机制,没有备份策略。解决:建表时加IsDeleted字段,所有删除操作改成UPDATE Ledger SET IsDeleted=1 WHERE Id=@Id,查询时统一加WHERE IsDeleted=0。另外每天定时把ledger.db复制一份到备份目录,用File.Copy就行,SQLite 单文件备份就这么简单。别等出事再找后悔药,那时候数据早被 WAL 覆盖了。

6. 把台账系统做成可交付源码的几个收尾技巧

6.1 用配置文件管理连接字符串和导出路径

源码交付给别人的时候,硬编码的连接字符串和路径是大忌。我习惯在项目里放一个appsettings.json,用Microsoft.Extensions.Configuration读。

using Microsoft.Extensions.Configuration; var config = new ConfigurationBuilder() .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .Build(); string connStr = config.GetConnectionString("LedgerDb"); string exportPath = config["ExportPath"];

对应的appsettings.json:

{ "ConnectionStrings": { "LedgerDb": "Data Source=ledger.db;Cache=Shared;Pooling=True;Default Timeout=30;" }, "ExportPath": "D:\\LedgerExport" }

这样部署到不同机器,只改 json 文件,不用重新编译。reloadOnChange: true让配置改了之后不用重启程序,对现场调试很友好。

6.2 防止反编译:源码保护的最低成本方案

C# 编译出来是 IL,用 dnSpy 能直接看回源码。台账系统里如果有连接字符串或者业务逻辑不想被人看到,至少做一层混淆。免费方案用ConfuserEx,命令行跑一下就行。

ConfuserEx.CLI.exe -n ledgersystem.csproj -o .\obfuscated

混淆后类名方法名变成乱码,反编译出来可读性极差。但注意别混淆 Dapper 的实体类,属性名被改了映射会失败。在 ConfuserEx 的配置里把实体类所在命名空间排除掉。如果预算够,上.NET Reactor,保护强度更高,但收费。

6.3 一个验证台账系统是否合格的检查清单

交付前我会跑一遍这个清单,每条都过了才敢说“能用”:

检查项合格标准验证方法
并发写入两人同时保存不报错开两个客户端同时点保存
查询响应五万条按时间筛选 < 1 秒造数据后秒表计时
导出内存三万条导出内存峰值 < 500MB任务管理器观察
断电恢复拔电后重启数据不丢WAL 模式下已提交事务不丢
误删恢复软删除可还原手动改 IsDeleted 字段
配置外置换机器只改 json拷贝到另一台电脑运行

这张表我每次交付前都过一遍,翻车次数从最初的每周一次降到半年零事故。台账系统这东西,功能写出来只算完成一半,剩下那一半全在稳定性和可维护性上。我现在的习惯是,任何删除操作先写软删除,任何批量操作先包事务,任何界面查询先想好异步。这些习惯不花哨,但能让你半夜不被电话叫醒。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 21:40:17

CentOS Stream 9根分区LVM在线扩容实战:lvextend与xfs_growfs详解

如果你手头有一台 CentOS Stream 9 服务器&#xff0c;某天突然收到根分区使用率 97% 的告警&#xff0c;日志写不进去、服务开始报错、连 SSH 都卡顿&#xff0c;而生产环境又不可能说重启就重启——这时候你需要的正是 LVM 在线扩容。 基于 LVM 的逻辑卷管理&#xff0c;Cen…

作者头像 李华
网站建设 2026/10/7 21:39:45

Linux常用命令实战指南:从文件操作到系统排查一次理清

很多人第一次打开Linux终端&#xff0c;面对那个黑底白字的窗口&#xff0c;心里其实有点慌。满屏的英文指令&#xff0c;也不知道该敲什么&#xff0c;更不敢乱敲&#xff0c;生怕一个回车下去系统就没了。但用久了你会发现&#xff0c;Linux的命令并不需要像背单词一样去记&a…

作者头像 李华
网站建设 2026/10/7 21:39:37

MetaGPT多智能体框架实测:从需求到代码的软件工程流水线

1. 为什么"AI写代码"还不够&#xff0c;MetaGPT要做"AI软件公司" 先说结论&#xff1a;MetaGPT不是一个普通的AI编程助手&#xff0c;它是把软件开发当成一条流水线来组织的智能体框架。我第一次看到这个项目时&#xff0c;第一反应是"又是一个套壳的…

作者头像 李华
网站建设 2026/10/7 21:36:20

Transformer-BiLSTM混合模型用于多特征时序预测

简介&#xff1a;本资源是一套基于PyTorch实现的Transformer-BiLSTM多特征时间序列预测完整方案&#xff0c;面向机器学习与深度学习初学者及工程实践者&#xff0c;适用于风电功率预测、光伏发电量预测、设备剩余寿命评估、环境浓度趋势推演等典型回归任务。压缩包共11个文件&…

作者头像 李华
网站建设 2026/10/7 21:35:36

Spring Boot+Vue全栈实战:一站式老年服务平台部署与业务闭环

简介&#xff1a;这是一套基于SpringBootVue的老年一站式服务平台毕业设计项目&#xff0c;适合Java方向毕业生、课程设计或期末大作业使用。项目包含完整的前后端代码与数据库脚本&#xff0c;覆盖用户端与后台管理端&#xff0c;界面简洁、操作路径清晰&#xff0c;并配有详细…

作者头像 李华