资讯详情 C# WinForms+SQLite设备管理系统开发:台账、维修与报表全攻略
📅 2026/10/13 10:29:11
简介这是一套基于C#与ASP.NET的Web设备管理系统完整源码工程主要面向C#初中级学习者、高校软件相关专业学生以及需要快速搭建设备管理后台的开发者。系统覆盖设备档案维护、借用归还登记、维修工单处理和报表统计等典型管理环节源码中充分体现MVC分层设计、ADO.NET数据访问、登录验证与权限控制等实用知识点并附带SQL Server数据库备份文件可还原后直接运行体验。压缩包共241个文件其中94个C#源文件与14个ASP.NET页面是核心代码另有DLL库、数据库备份、样式表、资源文件及Visual Studio解决方案文件结构清晰整体仅4.69MB。已有716人学习使用通过这套工程可直观了解完整业务系统的表结构设计、页面跳转逻辑和增删改查实现方式也便于进一步扩展维修工单、设备保养等模块是实践C#企业级Web开发的合适参照。1. C# 设备管理系统从 Excel 台账到可追溯的设备全生命周期做设备管理最怕的不是设备多而是台账乱。我曾接手过一个几十台设备的维护任务原来的记录分散在 Excel、纸质维修单和个别同事的脑子里每次盘点都要翻半天到了审计的时候更是找不全数据。后来我整理了一套基于 C# 的 Windows 桌面端设备管理系统把设备采购、领用、维修、报废全部串成一条可追溯的记录链。这套系统的核心价值是不用昂贵的商业软件用 C# 配合本地数据库就能把设备台账、状态追踪和统计报表跑起来适合中小型公司的设备管理员、车间班组长以及想练手完整业务逻辑的开发者。下文把整个系统怎么拆、怎么搭、参数怎么设、坑在哪都讲清楚。2. 系统架构与技术选型为什么用 WinForms SQLite 而不是别的组合2.1 WinForms 依然是设备管理这类内部工具的最优解选型不能跟风。设备管理系统本质是给少数内部人员用的 CRUD 加报表工具使用频率高、并发极低、界面以表单和表格为主。我见过有人非要用 Web 前端加后端服务结果部署在内网还得配 IIS 和数据库服务维护成本陡增也见过用 WPF 的界面确实漂亮但数据绑定和异步处理的复杂度对这类小项目来说有点用力过猛。WinForms 在 .NET 6/8 环境下对系统资源占用低DataGridView 直接绑定数据源事件模型简单直接开发效率极高。系统每次启动自动检查数据库文件不存在就创建关机即走不需要安装任何服务端组件。这套方案我在模拟项目X里反复验证过一个 2000 条设备记录以内的项目WinForms 的加载速度完全可以接受没必要引入重量级框架。2.2 SQLite 作为本地文件库的边界与参数调优数据库我选的是 SQLite而不是 SQL Server 或 MySQL。原因很直接设备管理系统的数据量通常在几万条以内单机使用居多SQLite 一个 .db3 文件就能装下全部数据备份时直接复制文件恢复时直接替换文件。但 SQLite 不是无脑用就好有几个参数必须先确认。我一般把连接字符串写成这样string connStr Data Sourcedevice_manage.db3;Version3;PoolingTrue;Max Pool Size10;Foreign KeysTrue;;逻辑说明连接字符串里显式声明了Foreign KeysTrue这是 SQLite 默认关闭的外键约束开关。如果不打开在业务代码里删了某个分类 ID台账表里的分类字段就会变成悬空引用。PoolingTrue让连接复用避免频繁开关数据库文件带来的 IO 抖动。Max Pool Size10对于单机桌面应用足够设置过大反而会占用句柄。参数说明SQLite 写入性能在单用户桌面场景下不是瓶颈但如果未来要支持多人同时写入就要考虑WAL日志模式。你可以在启动时执行PRAGMA journal_modeWAL;让读写互不阻塞代价是目录下会多出 -wal 和 -shm 两个临时文件备份时要把它们一起处理。2.3 数据表设计一张主表 三张字典表就是完整的骨架整套系统的表结构非常简单核心是设备台账主表加上分类、状态、维修记录三个字典或明细表。分类表和状态表用来约束录入避免同一种设备被写成空压机和空气压缩机两笔记录。维修记录表则是一对多关联主表保存每次故障的现象、处理人和费用。这里主表的字段设计直接决定了报表能不能统计出来我用的结构如下字段名类型说明IdINTEGER PRIMARY KEY AUTOINCREMENT设备唯一编号AssetCodeTEXT资产编码建议 YYYY 流水号DeviceNameTEXT设备名称CategoryIdINTEGER关联分类字典表StatusIdINTEGER1 在库 2 已出库 3 维修中 4 报废PurchaseDateTEXT采购日期UserNameTEXT当前领用人或责任人RemarkTEXT备注资产编码为什么要自己生成 YYYY 加流水号而不是依赖自增 Id因为自增 Id 删掉一条记录后会出现空洞将来盘点对不上账。编码字段独立出来之后即使中间删除数据编码也不重排符合固定资产管理的习惯。这个表里尽量避免存金额以外的冗余统计字段维修费用累计应当在报表时通过子查询算出来而不是在每笔记录上维护一个累计值。3. 台账与状态流转功能落地核心窗体与业务约束怎么写3.1 设备录入窗体的必填校验与着装校验逻辑录入窗体是整个系统里用户看得最多的界面校验在这里做严后面就少很多脏数据。除了常规的必填项检查值得专门写的是资产编码唯一性和状态合法性。前者用 SQL 的 COUNT 语句查重后者则是防止用户把一台还在库的设备直接选成维修中必须要求先领用人出现故障再进入维修状态。这里有三个关键校验点我在代码里写成了私有方法private bool ValidateInput() { if (string.IsNullOrWhiteSpace(txtAssetCode.Text)) { MessageBox.Show(资产编码不能为空); return false; } if (string.IsNullOrWhiteSpace(txtDeviceName.Text)) { MessageBox.Show(设备名称不能为空); return false; } if (string.IsNullOrWhiteSpace(cboStatus.Text)) { MessageBox.Show(请选择设备状态); return false; } return true; }逻辑说明IsNullOrWhiteSpace比IsNullOrEmpty更严格能拦截掉用户只敲空格的情况。资产编码的格式校验建议用正则^\d{4}-[0-9A-Za-z]{4,8}$限定年号和流水号的有效字符集避免中文和特殊符号混入编码。这些校验全部在点击保存按钮的 Click 事件里触发不要在 TextChanged 里实时检查否则录入长编码时会被反复提示。参数说明状态下拉框的数据源直接绑定字典表绑定用的是DisplayMember和ValueMember两个属性保存在数据库里的只有状态 Id界面显示的是状态名称。这样做的好处是以后要加闲置外借等新状态只需改字典表业务代码不用动。3.2 领用与退库操作如何避免脏状态设备管理系统里最容易出脏数据的位置是领用和退库。最常见的错误是直接 UPDATE 主表的状态和领用人不记录历史。我一般会加一张流转记录表每次领用、退库、维修、报废都插入一条历史记录。这样将来追责时能查出某个时间段这台设备的责任人和状态。核心方法如下private void ExecuteTransfer(int deviceId, int newStatusId, string userName, string remark) { using (var conn new SQLiteConnection(connStr)) { conn.Open(); using (var tx conn.BeginTransaction()) { var updateSql UPDATE device_main SET StatusIdstatus, UserNameuser, Remarkremark WHERE Idid; var insertSql INSERT INTO device_log(DeviceId, OldStatus, NewStatus, UserName, ChangeDate, Remark) VALUES(id, oldStatus, newStatus, user, date, remark); // 先查出旧状态再执行更新和插入日志 tx.Commit(); } } }逻辑说明这段代码把更新主表和插入日志放进同一个事务里只要任何一个失败两个操作一起回滚不会出现状态已经变了但日志没记的尴尬情况。事务的粒度一定要覆盖到这两步不能用两个独立连接分别执行。参数说明ChangeDate字段建议用本地时间DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss)而不是DateTime.Today因为同一天内可能有多次流转时间戳精确到秒才能正确排序。如果将来系统要跨时区使用再考虑把时间存成 UTC但设备管理属于纯内部应用本地时间足够。3.3 DataGridView 列表查询过滤条件拼装与防注入列表页是用户每天面对的界面必须支持按资产编码、设备名称、状态、日期区间过滤。很多人写查询时喜欢直接拼 SQL 字符串这在桌面内部工具里也不至于惹大祸但仍存在注入风险。我习惯的做法是使用参数化查询并把过滤条件动态拼接进 WHERE 子句private DataTable QueryDevices(string keyword, int statusId, string beginDate, string endDate) { var sql SELECT * FROM device_main WHERE 11; var conditions new Liststring(); var parameters new Dictionarystring, object(); if (!string.IsNullOrWhiteSpace(keyword)) { conditions.Add((AssetCode LIKE kw OR DeviceName LIKE kw)); parameters[kw] % keyword %; } if (statusId 0) { conditions.Add(StatusId sid); parameters[sid] statusId; } if (!string.IsNullOrWhiteSpace(beginDate) !string.IsNullOrWhiteSpace(endDate)) { conditions.Add(PurchaseDate BETWEEN b AND e); parameters[b] beginDate; parameters[e] endDate; } if (conditions.Count 0) sql AND string.Join( AND , conditions); sql ORDER BY AssetCode; // 执行 sql用 parameters 字典添加参数 }逻辑说明WHERE 11是拼 SQL 时的常用技巧它让后面的AND子句不用判断是否为首个条件代码结构更干净。参数化查询的关键是parameters字典和 SQL 里的kw、sid一一对应值单独传递不混进 SQL 文本。用户名键盘输入的引号、百分号都不会破坏语句结构。参数说明LIKE kw里的参数值需要手动在前后加%通配符这一点容易漏。很多人写成LIKE kw结果查询结果永远为空因为 SQL 把kw当成普通字符串了。另外日期过滤的边界值是BETWEEN包含当天如果要排除当天就得用和结合次日的零点。4. 维修记录与费用统计报表怎么算才不失真4.1 维修单与设备主表的关联设计维修记录表不能偷懒只存一个维修说明字段。我设计维修单表时固定了这样几个字段设备 Id、故障类型、维修描述、维修人、维修日期、费用金额。故障类型应当接字典表因为后续统计哪类故障占比最高时要按这个字段分组统计。维修人这里要注意它和领用人是两个维度维修人可能是厂家的售后工程师不是内部员工所以不要复用设备主表的 UserName。在代码里打开维修记录窗体时会先加载设备列表用户选一台设备然后保存一条维修单。这之间的外键关系就是device_main.Id对应device_repair.DeviceId。为了查询方便我习惯建立联合视图把设备编码和名称直接带进维修记录展示里CREATE VIEW IF NOT EXISTS v_repair_detail AS SELECT r.Id, m.AssetCode, m.DeviceName, r.FaultType, r.RepairDesc, r.Repairer, r.RepairDate, r.Cost FROM device_repair r LEFT JOIN device_main m ON r.DeviceId m.Id;逻辑说明视图的好处是查询代码不用每次 JOIN 两遍界面层直接把视图当成一张表来绑定。需要注意的是LEFT JOIN保证了如果维修记录对应的设备被删除了维修记录依然能查出来只是设备名称显示为空这种数据安全策略比INNER JOIN更稳妥。4.2 维修费用的累计统计SUM 与 GROUP BY 的坑维修费用是设备管理里最敏感的数字统计时最容易踩的坑是 GROUP BY 的粒度不对。例如想统计每台设备的年维修费用如果 GROUP BY 只用了 AssetCode 而 SELECT 却包含 DeviceName那么 SQLite 在某些模式下可能返回意外结果或者取出了随机一行。更稳妥的方案是分两步先按设备分组汇总费用再关联主表补上设备名称。我常用这样的查询SELECT t.AssetCode, t.TotalCost, m.DeviceName FROM ( SELECT DeviceId, SUM(Cost) AS TotalCost FROM device_repair WHERE RepairDate yearBegin AND RepairDate yearEnd GROUP BY DeviceId ) t LEFT JOIN device_main m ON t.DeviceId m.Id ORDER BY t.TotalCost DESC;逻辑说明子查询t里只保留了 DeviceId 和费用合计这样 GROUP BY 的列只有 DeviceId语义清晰不歧义。外层再 JOIN 设备主表取名称不会干扰聚合结果。yearBegin和yearEnd的取值规则是当 2025 年 1 月 1 日到 2026 年 1 月 1 日左闭右开这比用BETWEEN加 12 月 31 日更严谨因为如果你把结束日期写 2025-12-31就会漏掉当天 23 点 59 分 59 秒之后的记录。参数说明费用字段在 SQLite 里建议用REAL或NUMERIC(10,2)。用 REAL 可能在二进制浮点表示上有微小误差比如 0.1 加 0.2 不等于 0.3。对于金额统计我更建议把费用以分为单位存成 INTEGER例如 95.50 元存 9550。输出时再除以 100.0这样能彻底避开浮点误差。这个习惯是从一次对账差了两分钱的血泪教训里来的。4.3 状态分布统计与 DataGridView 导出 Excel状态分布统计直接按 StatusId 分组计数就能跑通但展示时用户看到的应该是一行维修中: 3台这种中文内容。这里要在内存里做一次映射把 Id 转成名称。至于导出 ExcelWinForms 本身没有原生的 Excel 组件常见做法是生成 CSV 文件 Excel 能直接打开。CSV 有个编码坑如果直接用File.WriteAllText默认编码写中文Excel 打开会乱码。解决办法是写入带 BOM 的 UTF-8 文件var sb new StringBuilder(); sb.AppendLine(资产编码,设备名称,故障时间,维修人,费用); foreach (DataRow row in dt.Rows) { sb.AppendLine(${row[AssetCode]},{row[DeviceName]},{row[RepairDate]},{row[Repairer]},{row[Cost]}); } System.IO.File.WriteAllText(repair_report.csv, sb.ToString(), new UTF8Encoding(true));逻辑说明UTF8Encoding(true)参数表示写入 BOM 头。这对 Excel 来说是识别 UTF-8 的标记不加的话 Excel 默认按 GBK 解读中文全变问号。另外 CSV 里的内容如果本身含逗号或换行需要用双引号包裹并转义内部引号这里可以封装一个EscapeCsvField方法处理避免导出后行错位。参数说明导出的文件路径不要写死到应用程序目录那种路径在用户的机器上可能没有写权限。我一般用SaveFileDialog让用户选位置或者默认写到Environment.GetFolderPath(Environment.SpecialFolder.Desktop)保证用户能立刻看到文件。5. 避坑指南设备管理系统实装中常见的六个翻车点5.1 备份时只复制数据库文件忘了 WAL 文件现象明明是刚备份的 .db3 文件在另一台电脑上打开却提示数据库损坏或缺少最近的数据。原因SQLite 在 WAL 模式下最近的写入先落在 -wal 文件里尚未合并进主库。解决备份前先执行一条PRAGMA wal_checkpoint(TRUNCATE);强制合并这个操作会把 WAL 文件内容写回主库此时再复制 .db3 就完整了。或者干脆使用 SQLite 的VACUUM INTO backup.db3命令生成备份这个命令能生成一个一致性快照不用手动处理 WAL。5.2 设备编码用自增 Id删除记录后账序全乱现象删除了 3 条设备记录后新设备编码从 4 开始资产管理员觉得不吉利也不规范。原因AUTOINCREMENT 本身不保证编码连续只保证唯一。解决放弃直接用 Id 作为资产编码改为DateTime.Now.Year加当天插入的自增序号。这个序号可以单独存一张seq_asset表每次生成新编码时在其中取下一个值。注意不要用 Max(Id)1 这种方式算序号并发插入时可能拿到重复值。5.3 维修窗体保存慢界面卡死现象用户点击保存后窗体要卡两三秒才返回。原因我在维修记录保存时同步执行了费用统计的 SQL统计涉及全表聚合数据量稍大就会卡顿。解决把统计查询拆到独立的刷新按钮或后台线程里保存操作只做单条 INSERT不要让主流程承担额外查询。如果一定要自动刷新就在保存后启动一个Task.Run去查查询结果通过委托回填到界面或者直接把 DataGridView 的刷新放在BackgroundWorker中。5.4 日期筛选查不出当天记录现象选择 2025-03-01 到 2025-03-31结果 3 月 31 当天的记录没了。原因日期字段实际存储带时间BETWEEN begin AND end中 end 按 2025-03-31 00:00:00 处理当天下午的记录被过滤掉了。解决范围过滤统一使用RepairDate begin AND RepairDate end 1天建议传入 endDate 后在其基础上加一天再拼接 SQL。这个坑我至少在三个项目里见过不同的人踩进去。5.5 状态为维修中的设备还能被领用现象设备明明在维修界面上的领用按钮依然可用。原因没有做状态机校验任何状态都能执行领用操作。解决在触发领用的方法里先查询当前状态如果状态 Id 为 3则弹出提示并终止操作。更完整的做法是写一个状态流转许可表定义每种状态允许跳转到哪些目标状态例如在库只能到已出库或维修中维修中只能到在库或报废已出库只能回在库。这套规则虽然简单却能把绝大多数操作错误挡在界面前。5.6 DataGridView 绑定数据源后新增行不见了现象用户点新增设备DataGridView 上出现了新行但数据库里没有。原因DataGridView 的AllowUserToAddRows默认是 true用户可以在界面上直接编辑新行但如果你没有把编辑后的行显式提交到数据库它只停留在内存里。解决把AllowUserToAddRows设为 false强制用户通过新增设备窗体录入数据避免这种假象。类似的还有AllowUserToDeleteRows防止用户直接点行首删除数据且不经确认。6. 进阶技巧把设备管理系统做成一个可移植的数据维护工具这套系统做到能跑能用只是第一步真正让它省心的是我后来加的三个细节。第一个细节是启动时自动做数据库结构校验。程序每次运行会检查device_main、device_repair、device_log三张表是否存在不存在就用CREATE TABLE IF NOT EXISTS语句补建。这样就彻底摆脱了软件要先装数据库工具的依赖任何一台 Windows 机器拷贝过去双击就能跑。第二个细节是支持从 Excel 旧台账导入初始化。旧数据通常是一张表头混乱的 Excel我的导入代码只做三件事把表头映射到 XML 配置文件、逐行读取字段、用临时 DataTable 做校验后批量 INSERT。校验失败的记录全部输出到一个文本文件里方便用户手动修正而不会导入一半就中断。第三个细节是记录操作日志。除了设备本身的流转日志我还写了一个app_log表记录谁在什么时间删除了哪条设备记录。这个表不提供删除功能数据只增不改作为最后的后悔药。有一次同事误删了一条设备数据就是从这张表里找到了原始编码和名称手动补了回去。从那以后我每次给别人部署这套系统都会强制确认日志表是否有写入权限并让用户养成定期导出备份的习惯。设备管理系统说到底治的是信息不落地的病数据库结构再漂亮没有严格的日志和备份机制数据一样会丢。希望这套思路和代码里的细节能帮你在自己的项目上少踩几个坑。本文还有配套的精品资源点击获取