ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C# WinForm实现医院挂号管理系统:从数据库设计到并发控制实战

C# WinForm实现医院挂号管理系统:从数据库设计到并发控制实战 简介基于C# WinForm开发的医院挂号管理系统完整源代码包采用C/S架构和MVC分层设计适合.NET初学者、计算机专业学生以及需要快速搭建管理系统的开发者参考。系统覆盖用户管理、科室管理、医生管理、门急诊挂号、挂号查询、打印挂号单等核心模块并对用户密码进行MD5加密数据操作通过存储过程完成还提供医生照片上传和统计报表的图文展示功能。压缩包共185个文件以64个C#源码文件、19个资源配置文件、SQL数据库脚本及CHM帮助文档为主另含可运行的exe、DLL及报表文件整体约10.03MB结构清晰便于按层查阅。已有1262人浏览学习源码包内包含项目源码、数据库脚本、设计文档和配置说明能够帮助学习者快速理解C/S开发思路与WinForm布局技巧也适合作为课程设计或毕业设计的参考资料。1. 挂号窗口的痛点需求从哪里来系统边界定在哪1.1 一个挂号窗口的日常我有一段时间在给一家社区卫生服务中心做信息化项目天天蹲在挂号窗口旁边观察流程。早上七点半窗口还没开门口已经排了一条队。窗口里的护士一边问挂哪个科、挂普通还是专家一边在Excel里登记患者姓名和手机号然后手写一张挂号单患者再拿着单子去诊室门口排队。到了中午Excel里有重复的患者有挂错科室的单子还有医生说这个病人我上午没排到号的争议。更要命的是退号——一个患者上午挂错了下午的专家号还在窗口等着办退费护士要翻半天Excel找记录。这不是个别现象。很多中小医疗机构挂号量不大不小上大型HIS系统嫌贵纯手工又天天出乱子。中间这批机构恰恰是最需要一套轻量级挂号管理系统的地方。用C#加WinForm写一个医院挂号管理系统就是在这种背景下落地最务实的方案开发周期短、部署环境要求低、窗口操作员上手也快。1.2 功能清单以及我刻意不做的功能做项目最怕需求蔓延。我在规划功能的时候列了一张要做的清单也列了一张坚决不做的清单。要做的是这六块科室和医生档案管理支持按科室检索医生医生排班和号源配置每天能挂多少号、哪些时段出诊提前配置好患者建档和查询身份证号做唯一标识姓名、手机号、既往病历号做辅助检索挂号与退号普通号、专家号分别处理挂号费收取与日终统计挂号记录能按时间、科室、医生三个维度汇总简单的角色权限控制挂号员、收费员、管理员各开各的入口。不做的是药房库存和进销存、电子病历书写、医保实时结算接口、患者手机端预约。这些不是不重要而是每一块拉出来都是独立系统硬塞进WinForm里只会让项目失控。提前把边界划清楚后面开发才有节奏跟医院那边谈需求的时候也能把这个版本做什么、后续怎么扩展一次讲明白。1.3 用户角色和权限边界系统的用户分成四类权限用一张用户表和一张角色字段就能控制住没必要上框架。管理员负责维护科室、医生、排班和用户账号挂号员负责窗口挂号、退号、患者建档收费员负责收取挂号费和诊查费、打印票据医生的账号只读主要用来查看当天自己的号源和已挂号患者列表。权限控制在WinForm里的实现比Web端简单得多。不需要拦截器窗体加载时判断当前登录用户的角色决定哪些按钮、Tab页、菜单项显示还是隐藏就够了。前端控制再加一条后端校验——在执行退号、改排班这类敏感操作的DAL方法里再判断一次角色防止有人直接拼SQL或者绕过界面调用数据层。2. 为什么是WinForm技术选型和项目分层的理由2.1 WinForm vs WPF vs Web门槛、部署和维护的现实考量很多人一听到WinForm就觉得老古董但在这个场景下它恰恰是合适的选择。我做选型时对比过三个方向方案开发门槛部署难度团队维护成本适合场景WinForm低拖控件绑事件就出界面低拷过去配个.NET Framework就能跑低资料多会的人多内网单机/局域网桌面应用WPF中高需要理解XAML和数据绑定低但样式踩坑多中学习曲线陡对界面视觉要求高的桌面应用Web/BS中高前端框架要求高中需要部署IIS或容器中高前后端都要维护多院区、多终端、远程访问医院的挂号窗口就那两三台电脑全在同一个局域网里没有异地访问的需求。用Web一方面要部署服务器另一方面操作员浏览器里开页面容易误关窗口导致挂号记录没提交。WinForm原生窗口加上Dialog模式不点确定就切不走反而符合窗口业务的强流程特性。.NET Framework 4.7.2在Windows系统自带连运行库都省得另外装。2.2 数据库选型从SQL Server到SQLite的取舍数据这块我直接锁定SQL Server但不建议所有场景都跟着学。如果你的项目最终要给一家几十个诊室、日均挂号量两三千的机构用SQL Server Express版本足够免费的容量上限10GB事实上用不完。如果只是校内课程设计、毕业设计Demo或者给一个小诊所搞个单机版SQLite完全够用数据库就是一个文件打包发布都不用建库脚本。我在项目里用的是SQL Server原因是这家单位后续大概率要接LIS、PACS的检查结果回传那些系统对外开放的接口基本都走SQL Server或Oracle趁早统一技术栈后面对接少折腾。连接字符串写在App.config里开发环境连本地LocalDB正式环境改server和database两行就行。2.3 项目分层UI、BLL、DAL、Common我用的是最经典的三层结构没有引入任何重量级框架Common实体类、枚举、公共扩展方法、全局登录用户信息DALAdo.Net直接写SQL返回值大多是DataTable或者List BLL业务规则校验比如退号的条件判断、流水号生成规则UIWinForm窗体只做界面展示和数据绑定不写SQL不直接new SqlConnection。为什么还要分一层BLL因为挂号退号这件事看似简单规则却不少。退号要求当天号才能退过了当天只能作废标记专家号退号要记录退号原因已就诊的号不允许退。这些规则如果散落在窗体按钮的Click事件里一个窗体写一遍第二个窗体复制粘贴改一改后面维护就是灾难。放在BLL里统一收口任何入口调用的都是同一套校验逻辑。3. 表结构设计撑起挂号和收费流程的底层骨架3.1 核心表有哪些我建了八张表分别是科室表Department、医生表Doctor、排班表Schedule、患者表Patient、挂号单表Registration、收费表Payment、退号表Refund、用户表SysUser。外加一条流水号表SerialNumber专门生成挂号单号这个单独拎出来是有讲究的后面说。科室和医生是一对多关系排班表关联医生和科室。患者表以身份证号做唯一索引但允许空身份证因为有些孩子还没办身份证用监护人身份证加姓名来建档。挂号单表关联患者、排班、操作员三条线是整个系统的交易主表。退号表相对独立记录原挂号单号、退号原因、经办人和退号时间方便财务对账。3.2 挂号单表系统里最重要的那张表挂号单表的核心字段大概长这样CREATE TABLE Registration ( Id INT IDENTITY(1,1) PRIMARY KEY, RegNo VARCHAR(20) NOT NULL UNIQUE, -- 挂号单号如20240517-0001 PatientId INT NOT NULL, -- 患者ID ScheduleId INT NOT NULL, -- 排班ID DeptId INT NOT NULL, -- 科室ID DoctorId INT NOT NULL, -- 医生ID RegType TINYINT NOT NULL DEFAULT 1, -- 1普通号 2专家号 Fee DECIMAL(8,2) NOT NULL, -- 应收费用 Status TINYINT NOT NULL DEFAULT 0, -- 0已挂号 1已就诊 2已退号 3已作废 OperatorId INT NOT NULL, -- 操作员ID CreateTime DATETIME NOT NULL DEFAULT GETDATE() );RegNo我坚持用时间前缀加四位序号格式20240517-0001。这么做有几个好处窗口报出单号患者能直接听懂财务对账看这个号就知道是哪天的业务分表迁移数据也方便。序号不直接用数据库自增主键而是单独建一个SerialNumber表用行锁更新保证并发下不重号。3.3 号源并发一张排班表里的booked_count排班表Schedule是最容易卡脖子的地方。初版设计我天真地以为挂号的时候查询一下已挂号数量如果小于总数就能挂。现场两台窗口机同时操作瞬间就挂出两个相同的号源因为两个事务都读到还剩一个号然后同时插入成功。后来改成在数据库层做原子操作挂号扣减号源和插入挂号记录放在一个事务里扣减号源用一条带条件的UPDATE完成UPDATE Schedule SET BookedCount BookedCount 1 WHERE Id ScheduleId AND BookedCount TotalCount;C#这边判断ExecuteNonQuery的返回值受影响行数为0就说明号源已满整个事务回滚不给用户生成挂号记录。这个思路和秒杀系统的库存扣减是同一个原理代码量不大但彻底解决并发问题。4. 核心功能实现挂号主流程的代码拆解4.1 挂号主界面三层区域的交互布局挂号窗体是操作员一天到晚盯着的界面布局一定要顺。我做成左、中、右三块区域左侧是TreeView按科室分组展示医生中间是DataGridView排班列表显示近7天的可挂日期和剩余号源右侧是患者信息面板身份证号文本框、姓名、手机号加上一个挂号按钮和一个退号按钮。TreeView的数据源绑定有个容易踩的坑WinForm的TreeView没有DataSource属性很多人上来就绑DataTable结果绑不上。正确做法是把数据查出来之后用循环手动组装TreeNode。科室作为父节点医生作为子节点子节点的Tag里存DoctorId。点击节点的时候从Tag里取医生ID去刷排班列表Tag数据类型是object取值记得强转我最初漏了强转跑起来经常报无效转换异常。4.2 扫码枪触发查询它就是键盘很多第一次做HIS相关系统的开发者会被扫码枪触发事件这个需求搞懵以为要装驱动、写串口通信。实际上市面上绝大多数一维码/二维码扫码枪默认就是模拟键盘输入的设备——光标聚焦在哪扫出来的字符就打到哪最后再自动敲一个回车。明白这一点代码就简单了。在患者编号文本框上挂KeyDown事件判断回车键触发查询private void txtPatientCode_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode Keys.Enter) { SearchPatient(txtPatientCode.Text.Trim()); e.SuppressKeyPress true; // 阻止回车切换到下一个控件 } }这里面最关键的一行是e.SuppressKeyPress true。如果不加扫码枪扫完自动回车焦点会跳到下一个控件用户还得用鼠标点回来非常别扭。患者查出来后自动把焦点定位到挂号按钮上操作员只需要确认信息再按一次回车就能完成挂号全程不用碰鼠标。4.3 挂号事务一个号源不能挂两个人挂号动作涉及多张表的写入我用C#的SqlTransaction把它们包在同一个事务里。流程是更新排班表的BookedCount判断是否扣减成功插入挂号单记录插入收费记录成功状态提交事务。using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); SqlTransaction tran conn.BeginTransaction(); try { int affected UpdateScheduleBookedCount(conn, tran, scheduleId); if (affected 0) { tran.Rollback(); MessageBox.Show(该号源已满请选择其他时段); return; } string regNo GenerateRegNo(conn, tran); // 从流水号表取号 InsertRegistration(conn, tran, regNo, patientId, scheduleId); InsertPayment(conn, tran, regNo, fee); tran.Commit(); MessageBox.Show($挂号成功单号{regNo}); } catch { tran.Rollback(); throw; } }有个细节要提醒SqlConnection和SqlTransaction都实现了IDisposable务必要用using确保释放和回滚。我见过有人手工写try-catch但finally漏了Rollback数据库连接池里全是挂着未提交事务的连接过一会儿系统就慢得像死机一样。4.4 退号和退费用状态字段管理生命周期退号不是简单删掉挂号记录那样财务流水全对不上。我在Registration表里用Status字段做状态机0已挂号、1已就诊、2已退号、3已作废。退号业务规则是状态为已挂号、未就诊且挂号日期是今天的记录才能退。满足条件后把状态改成已退号在Refund表插入一条退号记录同时调用退费接口把收费记录的状态改为已退费。如果挂号同时收了诊查费退费金额不是简单返还原价而是按项目拆分挂号费全退、诊查费看医生是否已经接诊决定。这些规则写在BLL的RefundService里窗口代码只管调用不参与判断。退号单号沿用原挂号单号后面加R后缀比如20240517-0001R。窗口和财务两边对账时按原单号反查就行不用维护两套编号体系。5. 界面优化的几个细节从能用到好用5.1 窗体缩放和Anchor/Dock为什么控件尺寸改不了WinForm窗体缩放尺寸改不了是个常年上热搜的话题。现象很典型窗口最大化以后DataGridView还是固定大小停在窗体左上角右边和下面空出一大块白。原因很简单WinForm窗体上的控件默认坐标是绝对定位的不会跟窗体大小联动。解决办法就两个属性Anchor和Dock。Anchor决定控件跟窗体哪条边保持相对距离比如表格控件想跟随窗体四边缩放就设Anchor为Top、Bottom、Left、Right组合左侧TreeView想跟随高度变化就设Anchor为Top、Bottom、Left。Dock更适合填充式布局比如顶部的工具栏条、底部的状态栏直接设Dock为Top或Bottom即可。缩放还有一个隐蔽的坑如果窗体的AutoScaleMode设置不当高分屏下整个界面字体会被缩放拉扯控件文字显示不全。统一设为AutoScaleMode.Dpi配合Font微软雅黑, 9pt在1080P和2K屏上实测都比较正常。5.2 DataGridView的样式和性能调优DataGridView是WinForm里最常用的表格控件默认样式非常丑深深浅浅的灰白格子字体小列宽失控。花点时间做三件事界面质感立刻不一样表头背景色设成浅蓝或深灰字体加粗单元格AlternatingRowsDefaultCellStyle设置交替行背景色AutoSizeColumnsMode设为Fill让列宽自动填满。大数据量加载会卡这是DataSet绑定方式最大的痛点。挂号系统的排班表通常只有几百行问题不大但日终统计可能要加载几千条记录。这时要把Enable双缓冲打开继承一个自定义DataGridView重写DoubleBuffered属性为true能明显减少刷新闪烁。列表绑定用DataTable的DefaultView不要循环往Rows里Add速度差一个量级。5.3 医疗风格美化和对话框交互细节医疗系统的界面风格我的经验是不要用深色主题挂号窗口整天灯光充足深色反而刺眼。主体用白色和淡青色系功能按钮用统一的蓝色FlatStyle危险操作比如退号、作废用橙红色警示但不扎眼。窗体的边框风格决定了用户能不能缩放。FixedDialog会禁用最大化按钮如果不用Scalable布局干脆用FixedDialog省得用户拉大窗体后布局出问题如果做了Anchor适配就用Sizable保留缩放功能。我个人推荐定点布局加FixedDialog挂号员不需要自主调整窗口锁死反而更稳妥。另一个容易忽略的是模式对话框。中途弹出来的子窗体比如患者建档、排班配置、退号确认一律用ShowDialog()设置StartPosition CenterParent确保操作顺序不被打乱。有些页面是历史遗留问题用了Show()窗口能随意切换结果单据建一半焦点丢了数据没保存。5.4 回车、焦点和Tab顺序给操作员减负窗口操作员一天要挂上百个号能少点几下鼠标都是好的。把窗体的AcceptButton设成保存按钮用户输入完信息直接回车就提交。Tab键顺序按操作流排扫码枪输入患者号 → 姓名确认 → 挂号按钮 → 收费按钮不要让焦点跳回左侧科室树那是查询用的不属于主流程。身份信息校验我放在文本框的Leave事件里就是热搜词里那个c#中文本框失去焦点。输入身份证号焦点一离开就校验18位合法性格式错误当场提示不用等用户点挂号才发现。校验通过后自动按身份证号去Patient表查是否已建档已建档的自动带出姓名和手机号未建档的弹出快速建档窗口只用填三个字段不打断挂号节奏。6. 开发中踩过的坑多线程、日期、打包和部署6.1 UI线程被数据库查询堵死async/await解法初版做完内部测试操作员反馈点查询之后窗口变成白色过两三秒才有反应这就是典型UI线程被数据库同步查询阻塞。WinForm的UI跑在主线程上主线程一旦阻塞整个窗口无法重绘看起来就像假死。解决方案是把耗时操作放到Task.Run里执行UI线程用async/await等待结果private async void btnSearch_Click(object sender, EventArgs e) { btnSearch.Enabled false; try { DataTable dt await Task.Run(() _scheduleService.GetRecentSchedules(doctorId)); dataGridView1.DataSource dt; } catch (Exception ex) { MessageBox.Show(查询失败 ex.Message); } finally { btnSearch.Enabled true; } }注意lambda表达式里不要直接操作控件把业务和数据访问包进去返回DataTable后回到UI线程再赋值给DataSource。C# 5.0之后的async/await写法很成熟用它替代BackgroundWorker代码量少一半逻辑也更清晰。6.2 日期查询漏数据的坑时间部分没清零日终统计的时候我第一次按挂号日期查当天的记录SQL写成WHERE CreateTime today结果数据时有时无明明是当天挂的号就是查不全。问题出在CreateTime字段包含时分秒SQL Server里的DateTime类型默认精度是毫秒级根本没有两条记录的时间完全相等。正确写法是半开区间WHERE CreateTime startDate AND CreateTime endDateC#侧把开始日期设为当天0点结束日期设为明天0点DateTime startDate DateTime.Today; DateTime endDate DateTime.Today.AddDays(1);这个坑在报表统计和号源筛选里都会遇到记住一个原则日期查询永远用半开区间别用等号判断。顺带说一句如果查询条件的列上有索引直接写CreateTime 和 还能让索引生效用DATEDIFF或CONVERT包一层就废掉了索引。6.3 打包部署ClickOnce和配套数据库脚本WinForm程序发布打包我用过两种方案。第一种是Visual Studio自带的ClickOnce发布优点是升级方便服务器放一个发布目录客户机点一下就自动更新缺点是需要网络共享或IIS而且每次版本号要手动递增。第二种是InstallShield Limited Edition做出来的安装包更正式能写注册表、能创建桌面快捷方式但配置流程啰嗦第一次玩容易卡在系统必备组件那一步。对于医院挂号系统这种内网应用我更推荐ClickOnce原因很直接医院窗口的电脑没有固定运维人员天天跑点位又分散通过共享目录自动升级能省一大半维护成本。部署时记得把数据库初始化脚本和连接字符串配置说明一起放进发布文档不然换一台新电脑装系统光建库就能折腾半天。6.4 连接字符串安全还有一个关于连接串安全的教训。最开始调试我图省事把sa账号密码直接写在App.config的ConnectionStrings里后来给医院的正式机部署完第二天就有同事提醒我测试机上的数据库登录日志里被人扫过查了一下是数据库端口暴露在公网弱口令被爆破。虽然不是程序的锅但这个习惯很危险。正规做法是开发环境用Windows身份认证集成安全性搞定正式环境如果必须用SQL账号密码弄成强密码连接串在配置文件里用DataProtectionConfigurationProvider加密。代码层面不需要特殊处理SqlConnection只认连接串系统在启动时自动解密。另外数据库账号的权限务必最小化——只给该库的读写权限不给dbowner更不要给sysadmin。这样就算连接串泄露攻击者拿到的也只是单库权限损失能控制住。再分享一个这项目做完之后的个人心得如果你打算把这套系统真正交给一线操作员用前期最好抽一两天时间蹲在窗口旁边看她们实际操作而不是自己在办公室想象需求。比如退号这个功能我原以为没人会用结果上线当天就有人退号还好当时把已就诊不能退的校验写了又比如扫码枪带回车这个行为第一批培训时就有操作员问为什么扫完卡自动跳到按钮上习惯了之后反而是她们最满意的功能点。用户不会看你的代码分层多漂亮只看一件事点一下页面有没有反应号挂没挂上钱对不对得上。把这些体验扣到底这套系统才算真正落地。本文还有配套的精品资源点击获取
返回列表