尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于C#的资产管理系统设计与实现:WinForms+SQL Server全解析

基于C#的资产管理系统设计与实现:WinForms+SQL Server全解析 简介企业资产流转涉及采购入库、领用、归还、维修、报废等环节传统Excel管理易出现数据冲突和状态混乱。以数据库为基石通过设计资产信息表、分类表、借用记录表等配合状态机约束和事务控制可保障数据一致性。C# WinForms桌面应用结合SQL Server存储过程实现了高效的双层交互。这种技术方案不仅适合课程设计或毕业设计也能迁移至MySQL等数据库。本文从数据库表结构、状态流转规则到分层源码实现详细拆解了一个完整资产管理系统的构建过程。 有人可能觉得资产管理系统这种项目网上多得是随便搜一下就是一堆源码压缩包。但我今天想认真聊聊这个基于C#的资产管理系统因为它的思路并不只是把增删改查套了一层皮而是把资产在企业里的真实流动过程做了出来。如果你正好在做课程设计、毕业设计或者刚入职想找一个练手项目来理解C# SQL Server怎么配合这份源码加数据库能给你省下很多自己摸索的时间。我会把整个项目怎么设计、数据库怎么拆表、源码怎么分层、跑起来遇到坑怎么办全部以我实际操作的角度拆开讲清楚。这包源码解压后是一个完整的Visual Studio解决方案里面带一个AssetsManager.sln数据库文件在Database目录下既有.mdf主文件也有.sql脚本文件。理论上只要你电脑装了VS和SQL Server按我下面说的步骤就能把系统跑起来。项目本身是WinForms桌面程序没有依赖复杂的前端框架对初学者极度友好同时又用到了多线程、委托、反射、事务这些C#里面比较有含金量的知识点想进阶的人也能在这份代码里找到值得学的东西。1. 项目整体设计与需求拆解1.1 资产管理系统到底要管什么我最早接到这个需求的时候对方是帮一家做设备租赁的小公司做内部工具。他们当时用Excel记录资产一台笔记本电脑被谁借走了、什么时候还的、现在在哪、维修过几次全靠人工在表里更新。问题很明显Excel多人同时编辑会互相覆盖资产状态靠颜色标记时间长了自己都记不清哪种颜色代表什么更别说月末对账了。资产管理的核心逻辑其实是一种状态流转。一件资产从采购入库到被某个部门领用再到归还、维修、调拨、报废它的状态在每个节点都要有记录。所以这个系统在设计功能时我没有只做一个资产信息登记表而是把所有和资产生命周期相关的动作都拆成了独立模块。系统的主要角色分两种管理员和普通员工。普通员工可以做资产查询、发起借用申请、查看自己名下的资产管理员可以管理部门和用户、审核借用申请、登记维修和报废信息、查看全量统计报表。这个权限划分很重要它决定了你后续表结构和界面设计的方向。1.2 技术选型为什么是C# WinForms SQL Server这个项目用C# WinForms而不是Web最直接的原因是需求方需要单机可跑、双击能用不想搭服务器。WinForms虽然看起来老气但在内部工具这个场景下非常稳部署也简单。你只要把生成的exe和配置文件拷过去装了.NET Framework的机器就能运行。数据库我选了SQL Server因为SQL Server的LocalDB模式可以免安装运行适合分发另外它的事务处理、视图、存储过程都比较成熟用来演示资产管理这种带状态流转的业务非常合适。如果你不想用SQL Server这套代码里所有数据库操作都是基于ADO.NET写的你只需要把SqlConnection换成MySqlConnection再改一下连接字符串就能移植到MySQL或者达梦数据库上这就是分层的优势。1.3 功能模块划分与用户角色整个系统在代码层面划分了几个清晰的功能模块系统管理用户管理、角色权限、日志查询资产档案资产分类、资产信息维护增删改查、资产状态管理业务流转领用/借用申请、审批、归还、维修登记、报废登记统计报表按分类统计资产数量、部门资产分布、维修费用统计用户权限这块我用了比较简单的角色枚举没有上复杂的RBAC模型因为对于这种规模的项目用枚举判断角色已经足够了。如果你要扩展成完整的权限控制可以在用户表里加一个角色ID再建角色权限关联表代码里用特性标注操作权限这样后期会灵活很多。2. 数据库设计与核心表结构2.1 数据表总体设计数据库是整个系统的基石我一开始就花了大量时间设计表结构。一个资产管理系统最常见的表包括用户表、部门表、资产分类表、资产信息表、资产借用/归还记录表、维修记录表、报废记录表、操作日志表。除此之外我还加了审批记录表用于记录领用申请每一级审批的状态。数量上不需要很多表但关系要理清。资产分类表是树形的可以支持多级分类比如电子设备下面分笔记本电脑和台式机再下面还可以分笔记本-ThinkPad这种品牌子类。用树形结构而不是拉平是为了将来盘点统计的时候可以按大类聚合。2.2 资产核心表字段设计拿资产信息表AssetInfo举例字段设计如下CREATE TABLE [dbo].[AssetInfo]( [Id] INT IDENTITY(1,1) PRIMARY KEY, [AssetNo] NVARCHAR(50) NOT NULL UNIQUE, -- 资产编号如ZC-2024-0001 [AssetName] NVARCHAR(100) NOT NULL, -- 资产名称 [CategoryId] INT NOT NULL, -- 分类ID关联AssetCategory [Specification] NVARCHAR(200) NULL, -- 规格型号 [BuyDate] DATETIME NULL, -- 购买日期 [OriginalValue] DECIMAL(18,2) NULL, -- 原值 [NetValue] DECIMAL(18,2) NULL, -- 净值 [Status] INT NOT NULL DEFAULT 0, -- 状态0在库1借用中2维修中3已报废 [Location] NVARCHAR(200) NULL, -- 存放位置 [PrincipalId] INT NULL, -- 当前责任人ID关联UserInfo [Remark] NVARCHAR(500) NULL, [CreateTime] DATETIME DEFAULT GETDATE() )这里有几个字段值得说明。AssetNo我设置了唯一约束并且用ZC-2024-0001这种编码格式不用自增ID直接做资产编号是因为资产编号要可读、要能打印成二维码。Status字段是核心所有的业务操作都在改这个字段的值同时插入对应的流转记录表。PrincipalId存的是当前责任人这个设计能在界面上快速显示这台电脑现在在谁手里。2.3 使用视图和存储过程简化业务我在项目里用了两个比较重要的视图。一个是View_AssetDetail它把资产表、分类表、用户表关联起来直接查视图就能拿到包含分类名称、责任人姓名的完整信息。另一个是View_AssetRecord把借用、维修、报废三类记录联合起来用于做生命周期时间线。存储过程主要用在借用归还这种多步操作上。比如借出资产时要同时更新资产状态、插入借用记录如果后续涉及审批还要插入审批记录。这三个操作必须在一个事务里要么全成功要么全失败。我写了一个sp_BorrowAsset存储过程来处理CREATE PROCEDURE [dbo].[sp_BorrowAsset] AssetId INT, UserId INT, OperatorId INT AS BEGIN BEGIN TRANSACTION UPDATE AssetInfo SET Status 1, PrincipalId UserId WHERE Id AssetId IF ROWCOUNT 0 BEGIN ROLLBACK TRANSACTION RETURN -1 END INSERT INTO BorrowRecord(AssetId, UserId, OperateType, OperatorId, OperateTime) VALUES(AssetId, UserId, 1, OperatorId, GETDATE()) COMMIT TRANSACTION END这里用到了事务避免了先改状态、再插记录失败导致数据不一致的问题。这个思路在C#代码里也能做用SqlTransaction控制但放进存储过程里有两个好处一是业务逻辑收敛在数据库端二是将来如果写别的客户端复用起来方便。3. 数据库设计细节与核心表结构3.1 资产、分类、用户与记录表的关系为了让后面写代码时不会绕晕我在这里把整个数据库关系整理一下。用户表UserInfo的字段很简单用户ID、用户名、密码Hash、真实姓名、部门ID、角色、手机号、是否启用。密码不能用明文我用的是MD5加盐的方式虽然MD5本身不够强但老系统这个强度基本够用。角色字段我用RoleType管理员是1普通员工是0代码里直接用枚举判断。分类表AssetCategory支持两级分类我设计时让它支持无限级用ParentId自关联。实际使用只需要两级但这样扩展性更好。分类ID在资产表中作为外键统计报表里按分类汇总时只需JOIN一次就能拿到分类路径。借用记录表BorrowRecord是最重要的业务表。它不只是存谁借了什么东西而是把每一次状态变化都记录下来。我的表结构如下字段名说明Id自增主键AssetId资产IDUserId使用人/借用人IDRecordType记录类型1借用2归还3维修4报废OperatorId操作人IDOperateTime操作时间Remark备注这个设计让每一条资产都有完整的历史轨迹。你在界面上点履历查的就是这张表按时间倒排的数据。好处是统计的时候非常灵活比如想查某个月借出了多少台设备直接按时间字段筛选。3.2 资产状态机的设计思路资产状态是整个系统的灵魂。我定义了一个AssetStatus枚举public enum AssetStatus { InStock 0, // 在库 Borrowed 1, // 借用中 Repairing 2, // 维修中 Scrapped 3 // 已报废 }状态之间不允许随意跳转。比如一台在库的资产不能直接变成已报废必须先经过借用/归还或者有一个明确的报废审批流程。我在代码里用一个静态类定义状态流转规则public static class AssetStatusRule { public static bool CanChange(int oldStatus, int newStatus) { switch (oldStatus) { case (int)AssetStatus.InStock: return newStatus (int)AssetStatus.Borrowed || newStatus (int)AssetStatus.Repairing || newStatus (int)AssetStatus.Scrapped; case (int)AssetStatus.Borrowed: return newStatus (int)AssetStatus.InStock || newStatus (int)AssetStatus.Repairing; case (int)AssetStatus.Repairing: return newStatus (int)AssetStatus.InStock || newStatus (int)AssetStatus.Borrowed || newStatus (int)AssetStatus.Scrapped; default: return false; } } }这样做的目的是防止界面上的误操作。比如有人直接把一台正在借出的电脑强制报废如果没有校验库存记录和实际资产就对不上了。实际开发中我在UI层、业务层都调用了这个规则彻底堵住了这条路径。3.3 并发与事务控制多用户同时操作时可能会出现两个人同时借用同一台设备的情况。数据库层面我用UPDATE语句的状态条件来防止超借UPDATE AssetInfo SET Status 1, PrincipalId UserId WHERE Id AssetId AND Status 0如果ROWCOUNT等于0说明资产已经不是在库状态了事务就回滚并提示该资产已被借出或状态异常。这种做法比先SELECT再UPDATE安全得多是典型的数据库乐观锁思路。在C#代码里我也在所有涉及多步写入的操作上加了TransactionScope或SqlTransaction。比如运行一个借用操作先调存储过程更新资产状态再插入记录最后写日志必须整体提交。有的初学者会在每个方法里单独提交结果记录写了但状态没改这种坑我踩过不止一次。4. 源码分层结构与核心业务实现4.1 分层的意义UI、BLL、DAL三层怎么拆这套源码的解决方案结构非常标准包含五个项目AssetsManager.UIWinForms界面层AssetsManager.BLL业务逻辑层AssetsManager.DAL数据访问层AssetsManager.Models实体模型层AssetsManager.Common公共工具类刚开始写项目时我所有SQL都直接写在窗体代码里虽然跑得通但改一个需求几乎要重写所有窗体。后来我重构成分层窗体只负责收集用户输入和展示数据业务逻辑放在BLL拼SQL和参数放在DAL。这样分工之后哪怕把界面从WinForms换成WPF业务代码也能直接复用。举个查询例子。DAL层的方法只接收查询条件返回DataTable或实体集合public static DataTable GetAssets(string keyword, int? categoryId, int status) { string sql SELECT a.*, c.CategoryName, u.RealName AS PrincipalName FROM View_AssetDetail a LEFT JOIN AssetCategory c ON a.CategoryId c.Id LEFT JOIN UserInfo u ON a.PrincipalId u.Id WHERE (keyword OR a.AssetName LIKE keyword OR a.AssetNo LIKE keyword) AND (categoryId IS NULL OR a.CategoryId categoryId) AND a.Status status; SqlParameter[] paras new SqlParameter[] { new SqlParameter(keyword, % (keyword ?? ) %), new SqlParameter(categoryId, (object)categoryId ?? DBNull.Value), new SqlParameter(status, status) }; return SqlHelper.ExecuteDataTable(sql, paras); }这里用到了SqlHelper这个公共类它封装了SqlConnection和SqlCommand统一提供ExecuteNonQuery、ExecuteDataTable、ExecuteScalar三个方法性能足够也减少了大量重复代码。4.2 登录模块与C#委托、事件的实际应用登录模块看着简单但我用了不少C#的进阶特性。窗体上用户名、密码框和登录按钮点击登录后通过BLL.LoginService.ValidateUser验证验证成功之后我通过一个全局静态类保存当前登录用户信息而不是每个窗体单独传参。登录成功之后的主窗体需要根据用户角色显示或隐藏菜单这里我用到了委托和事件的机制。具体做法是主窗体订阅一个UserLoginEvent当登录窗体验证通过后触发事件public static event ActionUserInfo UserLoginEvent; private void btnLogin_Click(object sender, EventArgs e) { UserInfo user LoginService.Validate(txtUserName.Text.Trim(), txtPassword.Text); if (user null) { MessageBox.Show(用户名或密码错误); return; } UserLoginEvent?.Invoke(user); this.DialogResult DialogResult.OK; }主窗体在构造函数里订阅这个事件然后根据user.RoleType调整菜单项的可用状态。这种方法比在登录窗体里直接new MainForm(user)松耦合得多将来如果要增加第三方登录只需要多触发一次事件即可。4.3 资产信息增删改查的实现细节增删改查是每个系统的基本功但写的时候有几个细节非常影响体验。新增资产时资产编号要自动生成。如果用户手动输入很容易重复或格式不统一。我的做法是在窗体加载时调用一个GenerateAssetNo方法根据当前最大编号加一生成新编号public static string GenerateAssetNo() { string maxNo SqlHelper.ExecuteScalar(SELECT MAX(AssetNo) FROM AssetInfo).ToString(); if (string.IsNullOrEmpty(maxNo)) { return ZC-2024-0001; } int seq int.Parse(maxNo.Substring(maxNo.LastIndexOf(-) 1)) 1; return ZC-2024- seq.ToString(0000); }保存时用SqlParameter传参防止SQL注入。这里特别要提醒千万别用字符串拼接去拼SQL哪怕是一个简单的删除操作也可能出事。删除资产我用了逻辑删除也就是给AssetInfo表加一个IsDeleted字段默认0删除时更新为1。这样做的好处是历史借还记录不会因为资产删除而变成孤儿数据统计历史报表时不至于断开。要在查询列表时统一过滤IsDeleted 0。4.4 借用归还流程与事务处理借用和归还是系统最核心的业务。流程上我分两步第一步创建借用申请第二步管理员确认。避免有人直接把状态改了然后重新登记一步到位。借用申请写入BorrowApply表包含资产ID、申请人、申请类型、申请说明、状态。管理员审批通过后调sp_ConfirmBorrow存储过程把资产状态改为借用中同时写入BorrowRecord表。这里状态更新和记录写入必须在同一个事务里。归还流程类似。员工在系统里选择归还系统先检查资产当前责任人是不是当前用户防止乱还。然后更新资产状态为在库责任人清空同时写入归还记录。整个过程同样放在事务里。我在界面层给这两个操作都做了比较完整的提示。借用时如果资产处于维修中或已报废界面上直接置灰不可选但业务层仍然有校验防止客户端被绕过。4.5 报表统计与Excel导出报表模块用到最多的就是DataGridView绑定数据然后按条件聚合。比如按部门统计资产数量和总价值SQL里用GROUP BYSELECT d.DeptName, COUNT(a.Id) AS AssetCount, SUM(a.OriginalValue) AS TotalValue FROM AssetInfo a JOIN UserInfo u ON a.PrincipalId u.Id JOIN Department d ON u.DeptId d.Id WHERE a.IsDeleted 0 GROUP BY d.DeptName导出Excel我用的是NPOI开源库而不是微软的Excel COM组件。原因是NPOI不需要在服务器或客户机上安装Office导出速度快生成的是真正的.xlsx文件直接双击能打开。如果你用原生COM部署到没有装Office的机器上一定会报检索 COM 类工厂中 CLSID 为 {00024500-0000-0000-C000-000000000046} 的组件失败这个坑我建议提前避开。4.6 摄像头扫码扩展用AForge识别资产二维码系统里还留了一个可选扩展模块通过摄像头扫码快速查找资产。我用了AForge.NET框架来控制摄像头具体设备管理用FilterInfoCollection枚举摄像头再用VideoCaptureDevice开启视频流。很多初学者问怎么设置摄像头视频属性和控制属性核心代码是这样的// 枚举摄像头 FilterInfoCollection videoDevices new FilterInfoCollection(FilterCategory.VideoInputDevice); // 选择第一个摄像头 VideoCaptureDevice videoDevice new VideoCaptureDevice(videoDevices[0].MonikerString); // 设置分辨率 videoDevice.VideoResolution videoDevice.VideoCapabilities .FirstOrDefault(r r.FrameSize.Width 1280 r.FrameSize.Height 720) ?? videoDevice.VideoCapabilities[0]; videoDevice.Start();视频帧通过NewFrame事件持续回调在回调里用Bitmap做识别。我这里用的是ZXing.Net来解析二维码。识别结果如果是合法的资产编号就直接跳转到该资产的详情页。这个功能不是必须的但如果你想把系统升级成手持PDA扫描这套代码就是很好的雏形。5. 数据库脚本、源码部署与常见问题排查5.1 拿到压缩包后的第一步导入数据库很多人一拿到源码数据库的zip就急着打开源码编译结果一堆错。正确顺序应该是先把数据库准备好。压缩包里的AssetsDB.sql脚本可以直接用sqlcmd执行也可以在SQL Server Management Studio里打开执行。如果带了.mdf文件也可以直接附加但附加后一定要检查登录名和账号权限。执行SQL脚本前建议先看一下脚本开头有没有CREATE DATABASE [AssetsDB]如果有就直接执行如果没有先在SSMS里手动建一个空数据库再用导入数据或者运行脚本。我这份脚本里已经带了建库语句直接执行后会生成库结构。5.2 修改连接字符串数据库弄好后接下来要改程序里的连接字符串。WinForms项目的连接字符串一般放在App.config文件的connectionStrings节点里。打开AssetsManager.UI项目下的App.configconnectionStrings add nameSqlConnectionString connectionStringData Source.\SQLEXPRESS;Initial CatalogAssetsDB;User IDsa;Password123456; providerNameSystem.Data.SqlClient / /connectionStrings这里要注意三点Data Source要改成你本地的SQL Server实例名别直接照抄.\SQLEXPRESS如果你用的是Windows身份验证把User ID和Password删掉改为Integrated SecurityTrue密码里有特殊字符的话比如或需要用XML转义比如amp;。改完后先编译一下如果报未能加载文件或程序集之类的问题多半是NuGet包没还原。右键解决方案选还原NuGet程序包再重新生成。5.3 运行时常见报错与排查方法我在实际测试这套源码时遇到过不少问题挑几个高频率的列出来方便你排查。问题一提示无法加载一个或多个请求的类型。有关更多信息请检索 LoaderExceptions 属性。这个错误出现的原因通常是反射加载程序集时某个依赖项版本不匹配或缺失。最常见的触发场景是程序集扫描时调用了Assembly.GetTypes()但某个类型引用了未安装的第三方库。解决方法是先找到LoaderExceptions属性里的具体异常信息可以临时在AppDomain.CurrentDomain.AssemblyResolve事件里输出加载失败的DLL名称。不过更直接的思路是检查是不是某层引用了最新版NuGet包而运行时环境比较旧。我的建议是统一目标框架为.NET Framework 4.7.2NuGet包版本不要乱升。问题二数据库连接失败报在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误这个问题九成是连接字符串配错了。你要先去SSMS里确认SQL Server实例名比如可能是localhost、(localdb)\MSSQLLocalDB或者DESKTOP-XXXXXX。如果是远程服务器还要检查SQL Server Browser服务是否启动防火墙是否放行1433端口。本机调试的话我建议用localhost配Windows身份验证最省事。问题三界面上中文显示乱码数据库里的中文乱码一是在建表时没指定排序规则二是程序读取时字符集不一致。我的建表脚本里统一用了NVARCHAR类型并且数据库排序规则是Chinese_PRC_CI_AS一般不会乱码。如果你用MySQL迁移记得把连接字符串里的charsetutf8加上不然中文必乱。问题四日志记录在异常后丢失日志模块我写了一个LogHelper用StreamWriter追加写入文本文件。如果你在事务里先写日志再回滚日志也可能被回滚掉。所以写日志的时机应该是在事务提交之后或者用独立的数据库连接来写而不能把日志放在事务里。这个坑我在调试借用流程时遇到过当时怎么也找不到操作记录后来才发现是事务回滚把日志一起回滚了。5.4 数据库备份与日常维护系统跑起来后备份是必须做的。SQL Server的备份可以用一条SQL完成BACKUP DATABASE [AssetsDB] TO DISK ND:\Backup\AssetsDB_20250101.bak WITH FORMAT;如果你想定时备份可以用SQL Server Agent建一个作业。但很多开发者电脑上装的是Express版没有SQL Agent服务那就只能自己写一个C#小工具用SqlCommand执行BACKUP DATABASE再放到Windows任务计划里跑。这个思路比装完整版数据库轻量很多。5.5 把系统迁移到MySQL或达梦数据库如果你不想用SQL Server想把这套源码改成MySQL或者国产数据库改造重点在DAL层。整套代码里所有SQL语句我尽量写得标准不依赖SQL Server特有的语法只有存储过程部分需要重写。另外SqlHelper这个类要换成MySqlHelper最快速的办法是引入MySql.Data包然后全局替换SqlConnection等类型。如果你用达梦数据库达梦提供了ODBC和JDBC驱动C#可以通过ODBC方式连接但要注意参数占位符从改成?。迁移时最容易遗漏的是自增主键语法。SQL Server用IDENTITY(1,1)MySQL用AUTO_INCREMENT达梦用IDENTITY(1,1)和SQL Server类似但也不是完全兼容。我建议在迁移时用ORM框架比如SqlSugar或EF Core可以屏蔽很多数据库差异。6. 项目迭代建议与个人心得这套系统我前后写了大概三周最开始只有基础档案管理后来根据企业反馈一点点加了借用归还、审批、履历、报表。很多同学喜欢一次把功能铺得很大我觉得没有必要。你先跑通主线流程资产入库、借用、归还、报废再考虑其他锦上添花的功能。如果你拿到代码之后想练手我建议先读懂几个核心文件SqlHelper是数据访问的基石AssetService里写了所有关于资产状态流转的方法MainForm里展示了怎么用事件实现窗体间通信。把这几个吃透剩下的界面逻辑哪怕删掉重写你也能搭出一个属于自己的版本。最后再分享一个小技巧这套源码虽然是一个zip包但你在本地跑通之后建议用数据脚本加代码仓库的方式做版本管理而不是只留一个zip。我遇到过太多次项目迭代几个月后原来那个zip包已经和最新代码差了十万八千里的情况。如果你真正想从这个项目里学到东西把它改成自己的加上自己习惯的日志框架、加权限、加Redis缓存这些都比单纯打开看一眼更有价值。本文还有配套的精品资源点击获取
返回列表