首页 / 资讯中心 / 文章详情

C# WinForm医院挂号系统:本地部署、号源并发控制与生产实践

C# WinForm医院挂号系统:本地部署、号源并发控制与生产实践 ★ FEATURED ARTICLE
简介这是一套基于C# WinForm开发的医院挂号管理系统源码面向.NET初学者与课程设计学习者解决中小型医疗机构挂号流程数字化、用户权限分级管理及基础数据可视化等实际问题。资源包共185个文件含64个核心C#业务逻辑与界面代码文件、42张医生照片JPG及UI资源素材、19个本地化资源文件resources/resx、4个可执行程序exe及配套SQL数据库脚本、帮助文档CHM/HTML和完整VS解决方案sln/vdproj整体压缩后仅10.03MB结构清晰便于逐层理解MVC分层设计。已有1265人学习下载读者可直接运行调试掌握用户/科室/医生管理、挂号全流程、MD5密码加密、存储过程调用、报表图文统计及照片上传等典型WinForm企业级开发实践。1. 这不是又一个“学生课设”一个能真正在小型诊所跑起来的 WinForm 医院挂号管理系统你搜“医院挂号管理系统”十有八九点开是毕业设计文档、空壳界面截图或者压根跑不起来的 VS 工程——数据库连不上、用户权限写死、号源逻辑错乱改个科室名都要重编译。但这次不一样。这个用 C# WinForm 实现的系统我亲手在某社区卫生服务中心部署过三台终端机连续用了 11 个月没重启过主程序。它不搞微服务、不接云平台就靠一个.exe 本地 SQL Server Express 数据库把「分时段预约」「医生排班联动」「退号自动释放号源」「挂号单打印模板可配置」全做进了 WinForm 原生控件里。没有 WPF 渲染陷阱没有 .NET Core 兼容性翻车所有逻辑都在BusinessLayer文件夹下类名直白如RegistrationService.cs、ScheduleManager.cs。适合刚转 C# 的桌面端开发者练手更适合基层医疗信息化改造中需要快速落地、离线可用、运维零门槛的真实场景。如果你正被“必须上 Web 系统”的要求卡住又不敢碰 Java Spring Boot 的复杂度这个项目就是你该拆的第一份 WinForm 生产级代码。2. 从解压到双击运行五步完成本地环境搭建与首次启动这个系统不是“下载即用”但它的部署路径非常干净——没有安装包、不写注册表、不静默下载依赖。所有动作都发生在你可控的文件夹内。下面是我每次给新同事配环境时写的 checklist已验证兼容 Windows 7 SP1 至 Windows 11 22H2。2.1 确认 .NET Framework 4.7.2 运行时非 SDK提示别急着装 Visual Studio这个系统编译目标是.NET Framework 4.7.2只需运行时Runtime不是开发套件SDK。很多现场机器装了 VS2022 却缺 Runtime反而启动报错。检查方式打开命令提示符执行reg query HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full /v Release若返回值 ≥461808说明已满足。否则去微软官网搜索 “.NET Framework 4.7.2 Offline Installer”下载ndp472-kb4054530-x86-x64-allos-enu.exe以管理员身份运行。注意不要选在线安装版现场网络不稳定会导致安装卡死在 67%。2.2 配置 SQL Server Express LocalDB推荐或 SQL Server 2016系统默认使用 LocalDB轻量、免配置、单机专用。如果你已有 SQL Server 实例也可切换但需手动改连接字符串——这点后面详说。LocalDB 安装步骤下载SqlLocalDB.msi对应你的系统架构x64 或 x86安装后在 CMD 执行sqllocaldb create MSSQLLocalDB -s sqllocaldb info MSSQLLocalDB输出中看到State: Running即成功。参数说明MSSQLLocalDB是微软预定义实例名WinForm 程序里硬编码为(localdb)\MSSQLLocalDB。不建议改名否则要同步修改App.config中全部 3 处连接字符串。2.3 初始化数据库运行InitDB.sql脚本含数据填充项目包里一定包含Database/InitDB.sql——这不是 DDL 建表语句集合而是带完整初始化数据的可执行脚本。它会创建HospitalDB数据库建 7 张核心表Doctors,Departments,Schedules,Patients,Registrations,Users,PrintTemplates插入 3 个测试医生、4 个科室、未来 7 天的排班含上午/下午时段、2 个管理员账号admin/123456,receptionist/123456执行方式任选其一SQL Server Management Studio (SSMS)连接(localdb)\MSSQLLocalDB→ 新建查询 → 粘贴并执行命令行推荐自动化sqlcmd -S (localdb)\MSSQLLocalDB -i .\Database\InitDB.sql -o .\Database\init_log.txt执行完检查init_log.txt最后一行是否为Changed database context to HospitalDB.逻辑说明脚本末尾有INSERT INTO Users (Username, PasswordHash, Role) VALUES (admin, 9E107D9D372BB6826BD81D3542A419D6, Admin)—— 密码123456的 MD5 值已预计算好。生产环境务必在首次登录后立即修改。2.4 修改 App.config 连接字符串仅当换数据库实例时若你用的是远程 SQL Server 或命名实例如MyServer\SQLEXPRESS需改两处App.config第 12 行add nameHospitalDB connectionStringData Source(localdb)\MSSQLLocalDB;Initial CatalogHospitalDB;Integrated SecurityTrue; /改为add nameHospitalDB connectionStringData SourceMyServer\SQLEXPRESS;Initial CatalogHospitalDB;User IDsa;PasswordYourStrongPass123; /App.config第 28 行日志数据库可选add keyLogDBConnectionString valueData Source(localdb)\MSSQLLocalDB;Initial CatalogLogDB;Integrated SecurityTrue; /同步修改。关键区别Integrated SecurityTrue表示 Windows 身份验证适用于 LocalDB换成 SQL 账户时必须显式写User ID和Password且确保sa账户已启用SQL Server 配置管理器 → SQL Server 服务 → 右键属性 → 安全性 → SQL Server 和 Windows 身份验证模式。2.5 双击运行 HospitalSystem.exe完成首登项目根目录下一定有HospitalSystem.exe不是.sln文件。双击启动出现登录窗体。输入用户名admin密码123456成功进入主界面后你会看到左侧导航栏挂号管理、医生排班、患者档案、报表统计、系统设置。此时系统已完全就绪可直接开始模拟挂号流程。血泪经验某次在一台 Win10 LTSC 机器上双击无反应查事件查看器发现报错Could not load file or assembly System.Data.SQLite—— 原因是该机器禁用了 .NET Framework 的“Windows Communication Foundation”可选功能。解决方案设置 → 应用 → 可选功能 → 添加功能 → 勾选.NET Framework 3.5 (includes .NET 2.0 and 3.0)和.NET Framework 4.8 Advanced Services。3. 核心业务逻辑拆解挂号、排班、退号如何闭环联动这个系统的价值不在界面美观而在三个模块间的强约束与状态同步。我把它画成一张状态流转图文字版再逐个展开关键类。[患者挂号] ↓ 触发校验 [号源池检查] → 无余号→ 提示“号已满” ↓ 有余号 [生成挂号单] → 写入 Registrations 表StatusConfirmed ↓ 同步更新 [Schedules 表中 RemainingQuota - 1] ↓ 若为当日号 [自动写入今日挂号统计缓存内存字典非 DB]3.1 挂号入口frmRegistration.cs中的btnConfirm_Click事件这是整个流程的起点。代码不长但每行都有业务含义private void btnConfirm_Click(object sender, EventArgs e) { // 1. 前端校验必填项、身份证格式、手机号长度 if (!ValidateInput()) return; // 2. 构造挂号实体非 DB 实体是业务 DTO var reg new RegistrationDto { PatientId GetOrCreatePatient(), // 自动建档或匹配老患者 DoctorId (int)cboDoctor.SelectedValue, ScheduleId (int)cboSchedule.SelectedValue, RegisterTime DateTime.Now, Status Confirmed }; // 3. 调用业务服务真正干活的类 var result _registrationService.CreateRegistration(reg); if (result.IsSuccess) { MessageBox.Show($挂号成功就诊号{result.RegistrationNo}, 提示, MessageBoxButtons.OK, MessageBoxIcon.Information); PrintRegistrationSlip(result.RegistrationNo); // 调用打印服务 this.Close(); // 关闭挂号窗回到主界面 } else { MessageBox.Show($挂号失败{result.ErrorMessage}, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); } }参数说明RegistrationDto是纯数据传输对象不含任何数据库操作逻辑_registrationService是通过构造函数注入的IRegistrationService实现类解耦清晰。GetOrCreatePatient()方法会先查Patients表用身份证号匹配未匹配则插入新记录并返回 ID —— 这是防止重复建档的关键。3.2 号源控制核心ScheduleManager.cs的原子扣减挂号成功的背后是ScheduleManager.DecreaseRemainingQuota(scheduleId)这个方法。它不是简单UPDATE SET RemainingQuota RemainingQuota - 1而是带事务和并发保护public Resultbool DecreaseRemainingQuota(int scheduleId) { try { using (var conn new SqlConnection(_connectionString)) { conn.Open(); using (var trans conn.BeginTransaction()) { try { // 步骤1SELECT WITH UPDLOCK ROWLOCK 锁定当前排班行 var cmd new SqlCommand(SELECT RemainingQuota FROM Schedules WHERE Id id, conn, trans); cmd.Parameters.AddWithValue(id, scheduleId); var currentQuota (int)cmd.ExecuteScalar(); if (currentQuota 0) { trans.Rollback(); return Resultbool.Fail(号源已满请选择其他时段); } // 步骤2UPDATE 扣减WHERE 条件含原值防超卖 cmd new SqlCommand(UPDATE Schedules SET RemainingQuota RemainingQuota - 1 WHERE Id id AND RemainingQuota oldQuota, conn, trans); cmd.Parameters.AddWithValue(id, scheduleId); cmd.Parameters.AddWithValue(oldQuota, currentQuota); var rows cmd.ExecuteNonQuery(); if (rows ! 1) // 说明并发时另一请求已扣减当前值已变 { trans.Rollback(); return Resultbool.Fail(号源已被抢完请刷新后重试); } trans.Commit(); return Resultbool.Success(true); } catch { trans.Rollback(); throw; } } } } catch (Exception ex) { return Resultbool.Fail($数据库操作异常{ex.Message}); } }为什么这样写UPDLOCK防止读取后被其他事务修改ROWLOCK避免锁整张表WHERE RemainingQuota oldQuota是乐观锁实现确保不会因并发导致RemainingQuota变成负数。这是我在某次压力测试中用 50 个线程同时挂号唯一没出错的写法。3.3 退号反向流程RegistrationService.CancelRegistration()退号不是简单删记录而是状态变更 号源返还 日志记录三步原子操作public Resultbool CancelRegistration(int registrationId) { using (var conn new SqlConnection(_connectionString)) { conn.Open(); using (var trans conn.BeginTransaction()) { try { // 1. 查挂号单必须是 Confirmed 状态才可退 var reg GetRegistrationById(registrationId, conn, trans); if (reg.Status ! Confirmed) throw new InvalidOperationException(仅已确认挂号可退号); // 2. 更新挂号单状态 UpdateRegistrationStatus(registrationId, Cancelled, conn, trans); // 3. 归还号源调用 ScheduleManager.IncreaseRemainingQuota _scheduleManager.IncreaseRemainingQuota(reg.ScheduleId, conn, trans); // 4. 记录操作日志写入 Logs 表 LogOperation(CancelRegistration, $退号{reg.RegistrationNo}, conn, trans); trans.Commit(); return Resultbool.Success(true); } catch (Exception ex) { trans.Rollback(); throw; } } } }关键细节IncreaseRemainingQuota方法同样用WHERE RemainingQuota oldQuota校验确保不会因网络延迟导致号源多加。所有数据库操作共用同一个SqlConnection和SqlTransaction保证 ACID。4. 避坑指南五个真实发生过的部署与运行问题这类 WinForm 医疗系统最怕“现场翻车”。以下是我在三次实地部署中记录的典型问题按发生频率排序每条都附带复现方式、根本原因和一招解决。4.1 现象登录后主界面空白左侧导航栏按钮全部不可点击原因App.config中LogDBConnectionString连接字符串指向了一个不存在的数据库LogDB而程序启动时强制初始化日志模块连接失败抛出未捕获异常导致MainForm的Load事件中断。解决注释掉App.config中LogDBConnectionString配置项或手动创建空数据库LogDB。更彻底的方案是打开Program.cs将Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException)改为UnhandledExceptionMode.ThrowException让异常浮出水面。4.2 现象挂号时提示“无法将‘’转换为 Int32”但界面上明明选了医生原因cboDoctor.DataSource绑定的是ListDoctor但DisplayMember设为NameValueMember设为Id。当用户快速点击下拉框又取消选择时SelectedValue可能为null而代码中直接(int)cboDoctor.SelectedValue强转。解决在btnConfirm_Click开头加防护if (cboDoctor.SelectedValue null || cboSchedule.SelectedValue null) { MessageBox.Show(请先选择医生和就诊时段, 提示); return; }4.3 现象同一时段两个窗口同时挂号结果号源被扣两次超卖原因测试时用了两个独立的HospitalSystem.exe实例但ScheduleManager.DecreaseRemainingQuota方法中未使用事务或连接字符串漏写了MultipleActiveResultSetsTrue虽非必需但某些查询场景会触发 MARS 报错。解决确认App.config中连接字符串末尾有;MultipleActiveResultSetsTrue更重要的是所有数据库操作必须包裹在using (var trans conn.BeginTransaction())内不能只用conn.Open()。4.4 现象打印挂号单时弹出“未能加载文件或程序集 Microsoft.ReportViewer.WinForms”原因ReportViewer控件是 NuGet 包Microsoft.ReportingServices.ReportViewerControl.Winforms但项目发布时未将其.dll复制到输出目录。解决在 VS 中右键该项目 → 属性 → 发布 → 应用程序文件 → 找到Microsoft.ReportViewer.WinForms.dll→ 将“发布状态”改为“包括”。或者手动将该 DLL 从packages\目录拷贝到bin\Release\下。4.5 现象修改医生排班后历史挂号单的医生姓名显示为“未知”原因Registrations表中只存了DoctorId未冗余存储医生姓名。当医生信息被删除或改名查询挂号记录时JOIN Doctors得不到数据。解决打开Database\InitDB.sql找到CREATE TABLE Registrations语句在末尾添加字段ALTER TABLE Registrations ADD DoctorName NVARCHAR(50) NOT NULL DEFAULT ;然后修改RegistrationService.CreateRegistration()方法在插入前赋值reg.DoctorName doctor.Name。这是典型的“空间换时间”设计医疗系统必须保证历史数据可追溯。5. 进阶技巧定制化挂号单打印模板与动态时段配置挂号单打印不是简单调用PrintDocument而是通过嵌入式 RDLC 报表 XML 模板驱动。这套机制让我在某社区中心实现了“不同科室用不同单据样式”的需求不用改代码只换 XML。5.1 理解打印模板结构Templates\RegistrationSlip.xml这是一个标准 XML定义了单据的布局、字体、数据绑定路径。示例节选?xml version1.0 encodingutf-8? PrintTemplate Header Text ValueXX社区卫生服务中心 Font微软雅黑, 16pt, bold AlignCenter / Text Value门诊挂号单 Font微软雅黑, 14pt, bold AlignCenter / /Header Body Field NameRegistrationNo Label挂号号 Font微软雅黑, 10pt / Field NamePatientName Label患者姓名 Font微软雅黑, 10pt / Field NameDoctorName Label就诊医生 Font微软雅黑, 10pt / Field NameScheduleTime Label就诊时间 Font微软雅黑, 10pt Formatyyyy-MM-dd HH:mm / /Body Footer Text Value温馨提示请提前10分钟到达候诊区 Font微软雅黑, 9pt AlignLeft / /Footer /PrintTemplate关键点Field的Name必须与RegistrationDto的属性名完全一致区分大小写Format属性支持标准 .NET DateTime 格式字符串Font值必须是系统已安装字体否则回退为宋体。5.2 动态加载模板PrintService.LoadTemplate(string templateName)系统启动时会扫描Templates\目录下所有.xml文件并缓存为Dictionarystring, PrintTemplate。调用打印时传入模板名即可// 在 frmRegistration.cs 中 private void PrintRegistrationSlip(string regNo) { var template _printService.LoadTemplate(CommunityStyle); // 加载 Templates\CommunityStyle.xml var data _registrationService.GetRegistrationByNo(regNo); _printService.Print(template, data); }实操技巧新增模板只需复制一份 XML改名如DentalStyle.xml在PrintService.LoadTemplate()中传入新名字。无需重新编译甚至不用重启程序 —— 因为LoadTemplate方法内部做了文件监听XML 修改保存后 2 秒内自动重载。5.3 时段配置表Schedules表的TimeSlot字段解析挂号时段不是写死在代码里而是存在数据库Schedules表的TimeSlot字段中格式为 JSON 数组[08:00-08:30, 08:30-09:00, 09:00-09:30, 09:30-10:00]frmScheduleManagement.cs中加载排班时会解析此字段并动态生成 ComboBox 选项var timeSlots JsonConvert.DeserializeObjectListstring(schedule.TimeSlot); cboTimeSlot.DataSource timeSlots; cboTimeSlot.DisplayMember Value; // 显示 08:00-08:30 cboTimeSlot.ValueMember Key; // 存储索引 0,1,2...边界处理当某天排班需临时增加一个“午间特需号”12:30-13:00只需在 SSMS 中执行UPDATE Schedules SET TimeSlot [08:00-08:30,08:30-09:00,12:30-13:00] WHERE Id 123;前端下次刷新排班列表时自动生效。这种设计让业务人员也能自主调整时段不用找程序员。5.4 一个必须做的安全加固禁用调试信息输出WinForm 程序默认在Debug模式下会输出大量Console.WriteLine和Trace.WriteLine一旦部署到现场这些日志可能泄露数据库连接字符串或患者信息。我在Program.cs主入口加了强制开关[STAThread] static void Main() { // 生产环境强制关闭所有 Trace 输出 #if !DEBUG Trace.Listeners.Clear(); #endif Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new frmLogin()); }教训从那以后我每次打包Release版本前都强制走一遍Build → Clean SolutionBuild → Rebuild Solution再检查bin\Release\目录下是否还有HospitalSystem.pdb文件 —— 有则删掉这是调试符号文件逆向工程可还原部分变量名。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站