简介:这是一套面向中小型医疗机构信息化建设与WinForm开发学习者的医疗设备管理系统源码,基于C#与WinForm技术栈实现,覆盖设备信息管理、合同管理、配件管理、维修管理、信息统计与打印等核心业务模块,适合需要参考完整医院系统架构的开发者、课程设计或毕业设计人群。压缩包共155个文件,约12.32MB,以30个cs源码文件、20个dll类库、9个resx与17个resources资源文件、8个config配置及6个exe可执行程序为主,另含sln解决方案、csproj工程文件与mdb数据库,结构完整可直接编译运行。资源内置管理员与普通账号两套登录体系,并附带需求文档与操作手册,便于理解业务逻辑与部署流程。目前已有777人学习下载,读者可借此掌握医疗设备管理系统的模块划分、数据库设计与WinForm界面组织方式,快速搭建二次开发或教学演示环境。
1. 医疗管理系统 CS 版:一个能直接跑在 Windows 上的门诊住院全流程项目
如果你手头正好有一份 WinForm 的医疗管理系统源码,打开解决方案却不知道从哪下手,或者想拿它改一套社区诊所、体检中心、小型民营医院能用的系统,那这篇拆解就是写给你的。这个项目是典型的 C/S 架构桌面端医疗管理软件,覆盖门诊挂号、医生接诊、药品与耗材库存、住院床位、收费结算、报表统计这几条主线,数据库走 SQL Server,界面用 WinForm 拖控件加 DataGridView 绑定,业务逻辑集中在 BLL 层,DAL 层用 ADO.NET 拼 SQL。它适合两类人:一类是刚学完 C# 和数据库、想找一个完整业务闭环练手的开发者;另一类是手上真有小型医疗机构信息化需求、想拿现成骨架二次开发的技术负责人。整套代码没有花哨的框架,反而更容易看懂数据怎么从窗体流到数据库再流回来。
2. 技术选型与整体架构:为什么这套老技术栈反而好改
2.1 分层结构:UI、BLL、DAL、Model 四层怎么切
这套项目最值得先看的是它的分层。很多 WinForm 项目写着写着就把 SQL 塞进按钮点击事件里,改一个字段要翻十几个窗体。这份源码相对克制,基本按 UI → BLL → DAL → Model 四层走。UI 层只负责取值、校验、调 BLL;BLL 层做业务规则,比如挂号时判断该医生当天号源是否已满、退费时校验是否已发药;DAL 层只做增删改查,返回 DataTable 或实体;Model 层是跟数据库表一一对应的实体类。
// BLL 层挂号业务示例 public class RegisterManager { private RegisterDAL dal = new RegisterDAL(); // 挂号:先校验号源,再写挂号记录 public string AddRegister(RegisterModel model) { // 1. 判断该医生该时段是否还有剩余号源 int used = dal.GetUsedCount(model.DoctorId, model.VisitDate, model.TimeSlot); int total = dal.GetTotalCount(model.DoctorId, model.VisitDate, model.TimeSlot); if (used >= total) { return "该时段号源已挂满"; } // 2. 判断患者是否重复挂号 if (dal.Exists(model.PatientId, model.VisitDate)) { return "该患者今日已挂号"; } // 3. 写入挂号记录,返回新挂号单号 return dal.Insert(model) > 0 ? "OK" : "挂号失败"; } }这段代码里GetUsedCount和GetTotalCount分别查已挂数量和排班总号源,Exists防重复挂号。参数DoctorId、VisitDate、TimeSlot三个字段联合定位一个号源池。返回值用字符串而不是抛异常,是这类项目的常见做法,UI 层直接弹 MessageBox 显示,简单直接。你要改业务规则,基本只动 BLL,不用碰窗体。
2.2 数据库设计:几张核心表撑起整个业务
医疗系统的复杂度一大半在表结构。这套项目的库表不算多,但主键外键关系清楚。核心表大致是:患者信息表、医生排班表、挂号记录表、处方主表与处方明细表、药品库存表、住院登记表、床位表、收费流水表。处方主表和明细表是一对多,挂号记录和处方通过挂号单号关联,收费流水再关联处方和住院单。
| 表名 | 作用 | 关键字段 |
|---|---|---|
| Patient | 患者档案 | PatientId、Name、IdCard、Phone |
| DoctorSchedule | 医生排班 | ScheduleId、DoctorId、VisitDate、TimeSlot、TotalNum |
| Register | 挂号记录 | RegisterId、PatientId、DoctorId、Status |
| Prescription | 处方主表 | PrescriptionId、RegisterId、TotalAmount |
| PrescriptionDetail | 处方明细 | DetailId、PrescriptionId、DrugId、Qty |
| DrugStock | 药品库存 | DrugId、DrugName、Stock、Price |
| Charge | 收费流水 | ChargeId、RegisterId、Amount、PayType |
设计上有个细节值得注意:库存扣减没有放在挂号或开处方时立即执行,而是放在收费确认之后。这样避免医生开了处方但患者没缴费,库存却被提前扣掉。这个顺序在真实业务里很关键,改的时候别搞反。
2.3 窗体组织:主窗体加模块化子窗体的导航方式
UI 层用一个主窗体做容器,左侧或顶部放导航按钮,点击后在主窗体的面板区域加载对应子窗体。子窗体分两类:一类是列表查询窗体,上面是查询条件区,下面是 DataGridView;另一类是编辑窗体,用 ShowDialog 弹出,确定后回写数据并刷新列表。
// 主窗体加载子模块的通用写法 private void OpenModule(Form childForm) { // 先清空面板里已有的窗体,避免叠加 foreach (Control c in panelMain.Controls) { if (c is Form) { c.Dispose(); } } childForm.TopLevel = false; childForm.FormBorderStyle = FormBorderStyle.None; childForm.Dock = DockStyle.Fill; panelMain.Controls.Add(childForm); childForm.Show(); }TopLevel = false是让子窗体变成控件嵌入面板的关键,Dock = DockStyle.Fill让它铺满。每次打开先 Dispose 旧窗体,否则反复切换会内存泄漏,这是 WinForm 项目里最常见的坑之一。参数childForm传具体模块窗体实例,比如new FrmRegister()。
3. 环境搭建与跑通:从数据库脚本到第一个登录窗口
3.1 开发环境与依赖准备
跑这个项目需要 Visual Studio(2019 或 2022 都行,装 .NET 桌面开发工作负载)、SQL Server(Express 版够用)、SQL Server Management Studio 用来执行建库脚本。项目本身是 .NET Framework 的 WinForm,不是 .NET Core,所以别用 VS Code 硬扛,调试体验差很多。常见做法是先用 SSMS 连上本地实例,把源码里附带的 .sql 脚本执行一遍,建库建表加初始数据。
-- 建库与基础数据示例 CREATE DATABASE MedicalDB; GO USE MedicalDB; GO CREATE TABLE Patient ( PatientId INT IDENTITY(1,1) PRIMARY KEY, Name NVARCHAR(50) NOT NULL, IdCard VARCHAR(18), Phone VARCHAR(20), CreateTime DATETIME DEFAULT GETDATE() ); -- 插入一条测试患者 INSERT INTO Patient (Name, IdCard, Phone) VALUES (N'测试患者', '110101199001010011', '13800000000');执行完脚本后,数据库里至少有患者、医生、药品几条基础数据,否则登录进去后各列表都是空的,容易误以为程序坏了。IDENTITY(1,1)让主键自增,NVARCHAR存中文姓名,这些细节在改表结构时要保持一致。
3.2 连接字符串配置与登录验证
数据库连不上是新手第一个翻车点。连接字符串一般放在 App.config 里,代码用 ConfigurationManager 读取。改的时候只改 server 和数据库名,别动 provider。
<!-- App.config --> <configuration> <connectionStrings> <add name="MedicalConn" connectionString="Data Source=.;Initial Catalog=MedicalDB;Integrated Security=True" providerName="System.Data.SqlClient"/> </connectionStrings> </configuration>Data Source=.表示本机默认实例,如果装的是命名实例就写成.\SQLEXPRESS。Integrated Security=True走 Windows 身份验证,不用填账号密码;如果 SQL Server 只开了混合验证,就换成User Id=sa;Password=你的密码。登录窗口的验证逻辑通常是查用户表比对密码,早期项目很多是明文或 MD5,改的时候建议顺手换成加盐哈希。
3.3 首次运行常见报错与处理
第一次 F5 运行,大概率会遇到几类问题。一是“未找到连接字符串”,检查 App.config 的 name 和代码里ConfigurationManager.ConnectionStrings["MedicalConn"]是否一致。二是“无法打开登录所请求的数据库”,多半是脚本没执行或库名写错。三是窗体设计器报错打不开,通常是某个自定义控件在构造函数里访问了数据库,设计时没有连接就崩,解决办法是把数据访问挪到 Load 事件里。四是 DataGridView 列乱序,检查 AutoGenerateColumns 和手动列绑定的顺序。
提示:跑通之前先把数据库脚本完整执行一遍,再确认连接字符串,最后才动代码。顺序反了会浪费大量时间在排查环境上。
4. 核心业务模块拆解:挂号、处方、收费、库存的代码链路
4.1 挂号与排班:号源校验和状态流转
挂号模块的入口是查询医生排班,选中某条排班后点挂号。核心在号源校验和挂号状态。挂号记录一般有“已挂号、已就诊、已退号”几个状态,退号要把号源释放回去。BLL 里除了前面看到的 AddRegister,还有一个 CancelRegister 方法。
// 退号:改状态并释放号源 public string CancelRegister(int registerId) { RegisterModel model = dal.GetById(registerId); if (model == null) return "挂号记录不存在"; if (model.Status == "已就诊") return "已就诊不能退号"; // 更新状态为已退号 int rows = dal.UpdateStatus(registerId, "已退号"); return rows > 0 ? "OK" : "退号失败"; }Status字段是状态机的核心,所有涉及挂号的查询都要带上状态过滤,否则统计当日挂号量会把退号也算进去。参数registerId是挂号记录主键。改这块时注意:如果业务要求退号退费,还要联动收费流水,别只改挂号状态。
4.2 处方开立与药品明细:主从表一起提交
医生接诊后开处方,界面是一个主表信息加一个明细 DataGridView。保存时要把主表和明细一起写库,通常用事务保证一致性。
// 保存处方:主表加明细,用事务 public bool SavePrescription(PrescriptionModel main, List<PrescriptionDetailModel> details) { using (SqlConnection conn = new SqlConnection(connStr)) { conn.Open(); using (SqlTransaction tran = conn.BeginTransaction()) { try { int presId = dal.InsertMain(conn, tran, main); foreach (var d in details) { d.PrescriptionId = presId; dal.InsertDetail(conn, tran, d); } tran.Commit(); return true; } catch { tran.Rollback(); return false; } } } }这里InsertMain返回自增的处方主键,再逐条写明细。事务保证要么全成功要么全回滚,避免出现有主表没明细的脏数据。参数main是处方头,details是明细集合。注意明细里的药品数量、单价、金额要在保存前算好,别指望数据库触发器。
4.3 收费结算与库存扣减:顺序不能乱
收费是业务闭环的最后一环。患者拿着处方来收费,收费成功后写收费流水,同时扣减药品库存。前面提过,扣库存必须放在收费确认之后。
// 收费:写流水并扣库存 public string Charge(int registerId, decimal amount, string payType) { // 1. 写收费流水 int rows = chargeDal.Insert(registerId, amount, payType); if (rows <= 0) return "收费失败"; // 2. 查出该挂号单对应的处方明细,逐条扣库存 var details = presDal.GetDetailsByRegister(registerId); foreach (var d in details) { int affected = stockDal.ReduceStock(d.DrugId, d.Qty); if (affected <= 0) { return "药品库存不足:" + d.DrugName; } } return "OK"; }ReduceStock里一般写成UPDATE DrugStock SET Stock = Stock - @qty WHERE DrugId = @id AND Stock >= @qty,用Stock >= @qty做乐观校验,返回影响行数为 0 就说明库存不够。参数payType区分现金、刷卡、扫码。改的时候如果要做退费,得反向加库存并写红冲流水,别直接删记录。
4.4 住院与床位管理:床位状态同步
住院模块相对独立,核心是床位表的状态字段:空闲、占用、预留、维修。办理住院时把床位改成占用,出院时改回空闲。
// 办理住院:占用床位 public string AdmitPatient(int bedId, int patientId) { BedModel bed = bedDal.GetById(bedId); if (bed == null) return "床位不存在"; if (bed.Status != "空闲") return "该床位不可用"; bedDal.UpdateStatus(bedId, "占用"); inpatientDal.Insert(patientId, bedId, DateTime.Now); return "OK"; }床位状态和住院记录要同步更新,否则会出现“床位显示空闲但实际有人住”的玄学问题。参数bedId是床位主键。出院时除了改状态,还要结算住院费用,这块通常和收费模块共用流水表。
5. 避坑与常见问题排查:那些让我加班到深夜的坑
5.1 连接字符串对了还是连不上
现象:App.config 里连接字符串看着没问题,运行就报“登录失败”。原因多半是 SQL Server 的 TCP/IP 协议没启用,或者实例名写错。解决:打开 SQL Server 配置管理器,把 TCP/IP 启用并重启服务;命名实例用.\实例名,别只写点。还有一种情况是 Windows 身份验证下当前用户没被授权访问该库,去 SSMS 里给登录名加 db_owner 角色。
5.2 DataGridView 绑定后列顺序和显示全乱
现象:查询出来的表格列顺序跟设计的不一样,还多出一些不想显示的列。原因:用了 DataSource 自动生成列,数据库字段顺序决定显示顺序。解决:把 AutoGenerateColumns 设为 false,在设计器里手动加列并设置 DataPropertyName 绑定字段,顺序自己控制。改字段名时记得同步改 DataPropertyName,否则列是空的。
5.3 窗体反复打开内存越用越多
现象:在主窗体里来回切换模块,任务管理器里内存一直涨。原因:每次 new 子窗体加进面板,旧的没释放。解决:加载新窗体前遍历面板 Dispose 掉旧窗体,或者干脆把子窗体做成单例缓存起来复用。前面 OpenModule 里那段 Dispose 就是干这个的,别省。
5.4 事务里抛异常但数据还是写进去了
现象:处方保存失败提示回滚了,但主表多了一条记录。原因:DAL 方法内部自己开了连接,没接收外层传进来的 conn 和 tran,导致明细写入用的是另一个连接,事务管不到。解决:所有参与同一事务的 DAL 方法都要接收SqlConnection和SqlTransaction参数,统一用外层的连接执行。这是分层项目里最隐蔽的坑。
5.5 中文乱码或日期格式报错
现象:患者姓名存进去变成问号,或者日期插入报“字符串转换失败”。原因:数据库字段用了 VARCHAR 而不是 NVARCHAR,或者代码里拼 SQL 时日期没按 yyyy-MM-dd 格式化。解决:中文列一律 NVARCHAR,参数化查询用 SqlParameter 传 DateTime 类型,别用字符串拼接日期。参数化还能顺带防注入,一举两得。
6. 二次开发改造:从能跑到好用,我踩过的那些弯路
拿到这套源码,多数人不会原样用,而是想改成自己场景的样子。我一般会从三个方向动手。第一是权限,原项目可能只有一个登录用户表,没有角色区分。改造思路是加角色表和角色权限表,登录后把权限缓存到内存,主窗体加载时按权限隐藏或禁用导航按钮。别在每个窗体里写 if 判断,集中在一个权限校验方法里,改起来才不痛苦。
第二是报表,医疗系统离不开日结、月结、药品消耗统计。原项目如果只有简单查询,可以引入 RDLC 报表或直接用 DataGridView 导出 Excel。导出这块我习惯封装一个通用方法,传 DataTable 和列头,内部用 NPOI 或 EPPlus 写文件,避免每个模块重复写导出代码。
第三是打印,处方笺、收费单、住院单都要打印。WinForm 里用 PrintDocument 加 PrintPreviewDialog,把要打印的内容画到 Graphics 上。这里有个血泪经验:打印坐标别写死像素值,不同打印机 DPI 不一样,写死了换台机器就错位。用毫米换算或者按纸张比例算。
// 通用导出 Excel 的封装思路 public void ExportToExcel(DataTable dt, string[] headers, string filePath) { using (var workbook = new HSSFWorkbook()) // NPOI { var sheet = workbook.CreateSheet("数据"); var headRow = sheet.CreateRow(0); for (int i = 0; i < headers.Length; i++) { headRow.CreateCell(i).SetCellValue(headers[i]); } for (int r = 0; r < dt.Rows.Count; r++) { var row = sheet.CreateRow(r + 1); for (int c = 0; c < dt.Columns.Count; c++) { row.CreateCell(c).SetCellValue(dt.Rows[r][c].ToString()); } } using (var fs = new FileStream(filePath, FileMode.Create)) { workbook.Write(fs); } } }headers是自定义列头,dt是查询结果,filePath由 SaveFileDialog 选。这样任何模块只要拿到 DataTable 就能导出,不用重复造轮子。
改造时验证方法很简单:每改一个模块,先把原流程完整走一遍——挂号、开方、收费、退号,看数据在几张表里是否一致。我习惯改完一个模块就用 SQL 查一遍关联表,确认没有孤儿记录。从那以后我每次动业务逻辑,都强制自己先跑一遍全流程回归,再去看代码写得漂不漂亮。希望这套拆解能帮你少走点弯路,把这份源码真正变成自己能用的系统。
本文还有配套的精品资源,点击获取