简介:这款医疗设备管理系统(WinForm)面向医院设备科、医疗信息化初学者及课程设计开发者,围绕设备台账、合同、配件、维修、统计和打印等业务,给出可运行的桌面端管理方案,并附需求文档与操作手册,便于理解流程和二次修改。压缩包为 rar 格式,共 155 个文件、约 12.32MB,含 30 个 cs 源码、20 个 dll 程序集、17 个 resources 资源、9 个 resx,以及 config、txt、png/jpg、exe、mdb 等,覆盖源码、界面资源、配置、数据库和说明文档。系统提供管理员与普通账号两套登录入口,默认密码可自行调整。已有 778 人学习或下载,读者可借此梳理设备全生命周期管理模块,结合文档完成环境搭建、功能验证和二次开发。
1. 医院设备台账不是表单堆叠:WinForm 医疗管理系统 CS 版的真实使用场景
一套医院系统如果只做设备台账,最怕的不是功能少,而是数据对不上。这套医疗管理系统 CS(WinForm)版本把设备入库、借用归还、维修记录和科室领用放在同一个桌面端里,适合中小型医院设备科或第三方运维团队做本地化部署。它不依赖浏览器,双击 exe 就能跑,数据库用 SQL Server,界面是典型的 WinForm 窗体加 DataGridView。医疗设备管理系统的核心不是界面多漂亮,而是每一台设备的当前状态、责任人、维修历史能在一个窗口里查清楚。下面按我拆项目的顺序,从分层目录、数据库初始化、核心模块代码一路讲到二次开发时怎么加字段、控按钮权限。
2. 分层与目录:医疗设备管理系统从登录窗体到台账模块的代码地图
2.1 解决方案分层:UI / BLL / DAL / Models 各管什么
WinForm 项目最容易写成“窗体里塞 SQL”,一开始跑得快,后面改一个字段要翻十几个窗体。这套医疗管理系统 CS 版按 UI、BLL、DAL、Models 四层拆,窗体只负责收集输入和展示结果,业务规则放在 BLL,SQL 集中在 DAL,实体和分页参数放 Models。常见做法是 UI 引用 BLL,BLL 引用 DAL 和 Models,DAL 只依赖 Models,不反向引用 UI。这样设备列表窗体不需要知道连接串,也不用手写SqlCommand。
目录结构大致如下,文件名和层级能直接对应到后面的代码:
MedicalDeviceManager/ ├─ MedicalDeviceManager.sln ├─ src/ │ ├─ MedicalDeviceManager.UI/ # WinForm 窗体与控件 │ │ ├─ Forms/ │ │ │ ├─ FrmLogin.cs # 登录窗体 │ │ │ ├─ FrmMain.cs # 主窗体与菜单 │ │ │ ├─ FrmDeviceList.cs # 设备台账列表 │ │ │ ├─ FrmBorrow.cs # 借用与归还 │ │ │ └─ FrmRepair.cs # 维修记录 │ │ ├─ Controls/ │ │ └─ Program.cs # 启动入口 │ ├─ MedicalDeviceManager.BLL/ # 业务规则与事务入口 │ │ ├─ DeviceService.cs │ │ ├─ BorrowService.cs │ │ └─ RepairService.cs │ ├─ MedicalDeviceManager.DAL/ # ADO.NET 数据访问 │ │ ├─ DbHelper.cs │ │ ├─ DeviceRepository.cs │ │ ├─ BorrowRepository.cs │ │ └─ RepairRepository.cs │ └─ MedicalDeviceManager.Models/ # 实体与查询参数 │ ├─ Device.cs │ ├─ BorrowRecord.cs │ └─ PagedResult.cs ├─ db/ │ ├─ 01_schema.sql # 建库建表 │ └─ 02_seed.sql # 科室、角色、初始管理员 └─ publish/ # 发布输出目录各项目职责可以按下面这张表对照,二次开发时先判断改哪一层,再动文件,减少到处补引用的概率:
| 项目 | 主要职责 | 依赖 |
|---|---|---|
| UI | 窗体事件、控件绑定、输入校验提示 | BLL、Models |
| BLL | 业务校验、事务边界、状态流转 | DAL、Models |
| DAL | 参数化 SQL、连接管理、结果映射 | Models |
| Models | 实体、分页参数、查询条件 | 无 |
2.2 目录与核心文件清单:窗体、服务、实体、SQL 脚本
登录窗体FrmLogin.cs只做三件事:读取账号密码、调用 BLL 的登录方法、把当前用户信息存到静态会话里。主窗体FrmMain.cs根据角色显示菜单,设备科能看到入库和维修,普通科室只能看借用。设备列表FrmDeviceList.cs里放 DataGridView、搜索框、状态下拉框和分页按钮,翻页时把pageIndex、pageSize、keyword、status传给DeviceService,不直接拼 SQL。
DbHelper.cs是数据访问层的入口,常见做法是读取App.config里的连接字符串,提供ExecuteNonQuery、ExecuteScalar、ExecuteDataTable和带事务的重载。所有外部输入都走SqlParameter,不拿字符串拼接。DeviceService.cs负责设备编号唯一性校验、状态是否允许修改、删除前是否有关联借用记录。BorrowService.cs负责借用和归还的事务,先查状态再更新,更新影响行数为 0 就回滚。
db/01_schema.sql是建库脚本,db/02_seed.sql初始化科室、供应商、角色和默认管理员。运行顺序不能反,先建表再插种子数据。种子脚本里的默认管理员只用于第一次登录,上线前必须改密码,这条我一般会写进发布检查表。
2.3 数据库表设计:设备台账、科室、供应商、维修记录
设备台账的核心表是Device,科室和供应商用外键关联,借用和维修各一张流水表。设备状态用整数表示:0 在库、1 借出、2 维修、3 报废。借用记录单独记BorrowStatus,避免设备表被历史记录撑大。建表脚本可以按下面这段执行,字段名和后面 C# 实体保持一致:
-- 科室表 CREATE TABLE Department ( DeptId INT IDENTITY(1,1) PRIMARY KEY, DeptName NVARCHAR(50) NOT NULL, IsActive BIT NOT NULL DEFAULT(1) ); -- 供应商表 CREATE TABLE Supplier ( SupplierId INT IDENTITY(1,1) PRIMARY KEY, SupplierName NVARCHAR(100) NOT NULL, ContactPhone NVARCHAR(30) NULL ); -- 设备台账主表 CREATE TABLE Device ( DeviceId INT IDENTITY(1,1) PRIMARY KEY, DeviceCode NVARCHAR(30) NOT NULL UNIQUE, -- 设备编号 DeviceName NVARCHAR(100) NOT NULL, DeptId INT NOT NULL, SupplierId INT NULL, PurchaseDate DATE NULL, Price DECIMAL(18,2) NULL, Status TINYINT NOT NULL DEFAULT(0), -- 0在库 1借出 2维修 3报废 Keeper NVARCHAR(30) NULL, -- 当前保管人 Remark NVARCHAR(200) NULL, CONSTRAINT FK_Device_Dept FOREIGN KEY (DeptId) REFERENCES Department(DeptId), CONSTRAINT FK_Device_Supplier FOREIGN KEY (SupplierId) REFERENCES Supplier(SupplierId) ); -- 借用归还流水 CREATE TABLE BorrowRecord ( BorrowId INT IDENTITY(1,1) PRIMARY KEY, DeviceId INT NOT NULL, Borrower NVARCHAR(30) NOT NULL, BorrowDate DATETIME NOT NULL DEFAULT(GETDATE()), ReturnDate DATETIME NULL, BorrowStatus TINYINT NOT NULL DEFAULT(0) -- 0借出 1已归还 ); -- 维修记录 CREATE TABLE RepairRecord ( RepairId INT IDENTITY(1,1) PRIMARY KEY, DeviceId INT NOT NULL, FaultDesc NVARCHAR(200) NOT NULL, RepairDate DATETIME NOT NULL DEFAULT(GETDATE()), FinishDate DATETIME NULL, Cost DECIMAL(18,2) NULL, RepairStatus TINYINT NOT NULL DEFAULT(0) -- 0维修中 1已完成 );DeviceCode加唯一约束,防止重复入库;Status用TINYINT比字符串省空间,查询也稳定。BorrowRecord和RepairRecord都只存DeviceId,需要显示设备名称时联表查,不冗余存设备名,避免设备改名后历史记录对不上。常见做法是给Device(DeptId, Status)和BorrowRecord(DeviceId, BorrowStatus)建索引,列表页按科室和状态筛选时不会全表扫。
3. 跑通第一步:SQL Server 建库、连接串配置与登录链路
3.1 开发环境与依赖:.NET Framework、Visual Studio、SQL Server
这套 WinForm 医疗管理系统建议用 .NET Framework 4.7.2 或 4.8,老机器兼容性好,Windows 10/11 自带运行时的情况也多。开发工具用 Visual Studio,安装时勾选“.NET 桌面开发”工作负载,里面包含 WinForms 设计器和 .NET Framework 目标包。数据库用 SQL Server,本地开发可以用 Express 版,生产环境按医院信息科要求部署。项目不依赖第三方 UI 库,DataGridView、TextBox、ComboBox 都是原生控件,减少发布时缺 DLL 的概率。
打开解决方案后先看每个项目的“目标框架”是否一致,UI 项目是 .NET Framework,BLL 和 DAL 也必须是同框架或兼容框架。常见翻车点是 DAL 用了 .NET Standard 类库,UI 引不进来,报一串找不到类型的错。把四个项目统一到同一个 .NET Framework 版本,再编译。
3.2 数据库连接串和初始化脚本
连接字符串放在 UI 项目的App.config里,DAL 通过ConfigurationManager.ConnectionStrings读取。不要写死在DbHelper里,换一台机器就要重编译。下面这段是本地开发常用的配置,Data Source改成实际实例名,Initial Catalog对应建库脚本里的数据库名:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <connectionStrings> <!-- 本地开发示例:Data Source 换成 .\SQLEXPRESS 或实际实例名 --> <add name="MedicalDb" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=MedicalDeviceDb;Integrated Security=True;" providerName="System.Data.SqlClient" /> </connectionStrings> </configuration>Integrated Security=True表示用 Windows 身份验证,适合本地开发。生产环境如果 SQL Server 和客户端不在同一台机器,常见做法是建一个只对业务库有读写权限的账号,把连接串改成User ID=...;Password=...;,并且不要把明文连接串提交到代码仓库。初始化时先在 SQL Server 里执行01_schema.sql,再执行02_seed.sql,最后在Department和Supplier表里补几条测试数据。执行脚本前确认当前数据库上下文,避免建到master里。
3.3 登录与主窗体启动的完整链路
登录按钮的事件里不直接查数据库,调用UserService.Login,返回用户实体或null。登录成功后把UserId、UserName、RoleCode存到静态会话类,主窗体根据RoleCode控制菜单可见性。下面这段是登录链路的核心代码,DbHelper内部用参数化查询,Program.cs先显示登录窗体,登录成功后再启动主窗体:
// Program.cs:先登录,再进主窗体 static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); using (var login = new FrmLogin()) { if (login.ShowDialog() != DialogResult.OK) { return; // 用户取消登录,直接退出 } } Application.Run(new FrmMain()); } // FrmLogin.cs:登录按钮事件 private void btnLogin_Click(object sender, EventArgs e) { var userName = txtUserName.Text.Trim(); var password = txtPassword.Text; // 实际项目里存哈希,不存明文 if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(password)) { lblMsg.Text = "账号和密码不能为空"; return; } var user = _userService.Login(userName, password); if (user == null) { lblMsg.Text = "账号或密码不正确"; return; } CurrentUser.UserId = user.UserId; CurrentUser.UserName = user.UserName; CurrentUser.RoleCode = user.RoleCode; DialogResult = DialogResult.OK; Close(); }ShowDialog返回DialogResult.OK才进主窗体,取消登录直接退出,不会出现“没登录也能进主界面”的漏洞。CurrentUser是静态类,只存当前会话必要字段,不存密码。Login方法里先按用户名查用户,再校验密码哈希,最后检查账号是否启用。参数说明:userName去空格后比对,password不记日志;RoleCode后面用来控菜单和按钮,不直接控数据库权限。
4. 核心模块拆解:设备入库、借用归还和维修记录的完整实现
4.1 设备入库与台账列表:DataGridView 绑定与分页查询
设备列表页的 DataGridView 不要直接绑定整张表,设备多了会卡。常见做法是分页查询,每页 20 或 50 条,搜索条件带设备编号、设备名称、科室和状态。下面这段是DeviceRepository的分页方法,用ROW_NUMBER()兼容 SQL Server 2012 及以上,返回DataTable给 UI 绑定:
public DataTable QueryDevicePage(string keyword, int? status, int pageIndex, int pageSize) { var sql = @" ;WITH T AS ( SELECT d.DeviceId, d.DeviceCode, d.DeviceName, dept.DeptName, d.Status, d.Keeper, d.PurchaseDate, ROW_NUMBER() OVER (ORDER BY d.DeviceId DESC) AS RowNo FROM Device d INNER JOIN Department dept ON dept.DeptId = d.DeptId WHERE (@Keyword = '' OR d.DeviceCode LIKE @Keyword OR d.DeviceName LIKE @Keyword) AND (@Status IS NULL OR d.Status = @Status) ) SELECT * FROM T WHERE RowNo BETWEEN (@PageIndex - 1) * @PageSize + 1 AND @PageIndex * @PageSize;"; var parameters = new[] { new SqlParameter("@Keyword", $"%{keyword}%"), new SqlParameter("@Status", (object)status ?? DBNull.Value), new SqlParameter("@PageIndex", pageIndex), new SqlParameter("@PageSize", pageSize) }; return DbHelper.ExecuteDataTable(sql, parameters); }keyword传空字符串时LIKE '%%'不筛选;status为null时查全部状态。pageIndex从 1 开始,pageSize建议 20~50,太大 UI 渲染会慢。UI 层拿到DataTable后设置dataGridView1.DataSource = table,不要手动一行行Rows.Add,否则排序和列宽都要自己维护。入库时先校验DeviceCode是否重复,再插入,插入成功刷新当前页。
4.2 借用归还事务:库存状态流转和并发处理
借用和归还必须在同一个事务里完成:先查设备当前状态,再更新设备状态,再写借用流水。两个人同时点“借出”时,只靠先查后更新会超借,常见做法是更新语句带状态条件,用影响行数判断是否抢到。下面这段是BorrowService.Borrow的核心逻辑:
public bool Borrow(int deviceId, string borrower, out string message) { using (var conn = DbHelper.CreateConnection()) { conn.Open(); using (var tran = conn.BeginTransaction()) { try { // 只有“在库”状态才允许借出,条件更新防止并发超借 var updateSql = @"UPDATE Device SET Status = 1, Keeper = @Borrower WHERE DeviceId = @DeviceId AND Status = 0"; var affected = DbHelper.ExecuteNonQuery(conn, tran, updateSql, new SqlParameter("@Borrower", borrower), new SqlParameter("@DeviceId", deviceId)); if (affected == 0) { tran.Rollback(); message = "设备已被借出或状态不允许借用"; return false; } var insertSql = @"INSERT INTO BorrowRecord(DeviceId, Borrower, BorrowStatus) VALUES(@DeviceId, @Borrower, 0)"; DbHelper.ExecuteNonQuery(conn, tran, insertSql, new SqlParameter("@DeviceId", deviceId), new SqlParameter("@Borrower", borrower)); tran.Commit(); message = "借用成功"; return true; } catch (Exception ex) { tran.Rollback(); message = "借用失败:" + ex.Message; return false; } } } }affected == 0说明设备已经不在库,事务回滚,不会写出脏流水。归还时同理,更新Device.Status = 0且Keeper = NULL,条件加上Status = 1,再把BorrowRecord.BorrowStatus改成 1、填ReturnDate。参数deviceId从列表选中行取,不要从界面文本反解析;borrower去空格并限制长度,避免超字段。
4.3 维修记录与折旧提醒:定时任务或启动检查
维修记录页在设备送修时把Device.Status改成 2,维修完成改回 0 并写FinishDate。折旧提醒不需要跑后台服务,主窗体启动时查一次超过使用年限仍在用的设备,弹一个提醒列表即可。下面这段 SQL 按采购日期算使用年限,DepreciationYears可以先放在配置表里,简单项目直接写 5 年:
-- 查询使用超过 5 年且未报废的设备,启动时提醒 SELECT DeviceId, DeviceCode, DeviceName, PurchaseDate, DATEDIFF(YEAR, PurchaseDate, GETDATE()) AS UsedYears FROM Device WHERE Status <> 3 AND PurchaseDate IS NOT NULL AND DATEDIFF(YEAR, PurchaseDate, GETDATE()) >= 5 ORDER BY PurchaseDate ASC;DATEDIFF(YEAR, ...)按年份差算,跨年当天可能有 1 天误差,设备管理场景够用。启动提醒不要用模态框连续弹,容易烦,常见做法是在主窗体底部加一个Label显示“有 N 台设备超过使用年限”,点开再看列表。维修中设备不计入折旧提醒的“在用”范围,避免刚送修就弹提醒。
5. 避坑与排查:WinForm 医院系统上线前最容易翻车的 5 个点
5.1 连接串写死导致换机器就报错
现象是开发机能登录,拷到设备科电脑上一点登录就报“未找到或无法访问服务器”。原因通常是连接串写死在DbHelper里,Data Source还是开发机的实例名,或者用了 Windows 身份验证但目标机器没有对应权限。解决办法是把连接串放App.config,发布时只改配置文件不重编译;生产环境建独立数据库账号,只给业务库的db_datareader、db_datawriter和执行存储过程的权限,不用sa。
5.2 DataGridView 直接改数据没有回写
现象是界面上改了设备保管人,关掉窗口再打开又变回原值。原因是 DataGridView 只绑定了内存里的DataTable,没有UPDATE语句,用户以为改的是数据库。解决方法是列表页设ReadOnly = true,编辑走独立窗体;确实要支持单元格编辑时,在CellEndEdit事件里拿主键更新,并检查影响行数。更稳的做法是禁止直接编辑,所有修改走“编辑”按钮打开详情窗体,保存时再校验状态。
5.3 借用归还并发导致库存负数
现象是两个人同时借同一台设备,两条借用记录都成功,设备状态却只改了一次。原因是先SELECT查状态再UPDATE,中间没有锁。解决方法是把状态条件写进UPDATE的WHERE,用影响行数判断,影响 0 行就回滚;事务里先更新设备再插流水,顺序不要反。高并发场景还可以给Device行加UPDLOCK,但中小医院设备科用条件更新已经够。
5.4 发布时缺少 DLL 或 .NET Framework 版本不匹配
现象是开发机双击 exe 正常,拷到目标机报“缺少某某 DLL”或“需要 .NET Framework 4.8”。原因是发布目录只拷了 exe,没有带依赖文件,或者目标机没装对应运行时。解决方法是发布时选“Release”,把整个publish目录打包;在目标机先装 .NET Framework 运行时,再运行 exe。用了第三方库就把 DLL 一起放进目录,不要依赖 GAC。发布前在一台干净虚拟机上跑一遍登录和列表查询,比在开发机反复点更有效。
5.5 数据库备份与还原的排序规则不一致
现象是脚本在开发库能建表,还原到另一台 SQL Server 上报排序规则冲突,联表查中文出现乱码或比较失败。原因是两边数据库排序规则不同,NVARCHAR字段联表时按不同规则比较。解决方法是建库时显式指定排序规则,或者还原时用“还原数据库”而不是只附加 MDF;跨库查询时在字段上统一COLLATE。上线前把建库脚本在目标实例跑一遍,别只还原数据。
6. 二次开发:把固定台账改成可配置的医院设备管理模块
设备属性经常变,今天加“注册证号”,明天加“维保到期日”,直接改Device表会让老代码到处报错。我一般加一张DeviceExtField字典表和DeviceExtValue值表,主表不动,列表页动态拼列。字典表存字段编码、显示名、控件类型,值表存DeviceId + FieldCode + FieldValue。新增字段只插字典数据,不改表结构,老设备没有值就显示空。
权限控制别只控菜单,按钮也要控。角色表加RoleCode,权限表存RoleCode + MenuCode + ButtonCode,主窗体加载时把当前角色能看的按钮设Visible = false,服务层再校验一次。前端隐藏不是安全边界,BLL 里根据CurrentUser.RoleCode判断是否允许报废、是否允许改价格。操作日志表记录UserId、Action、TargetId、ActionTime,借用、归还、报废三个动作必须写日志。
验证方法按“新字段能不能存、老数据能不能显示、无权限按钮能不能绕过”三条走。发布清单我一般固定五项:连接串是否指向目标库、脚本是否按顺序执行、默认管理员密码是否改过、发布目录是否包含全部 DLL、目标机运行时是否安装。从那以后我每次改完设备模块,都强制在干净环境走一遍登录、入库、借用、归还、维修、导出,再交付。希望帮到你。
本文还有配套的精品资源,点击获取