尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
C# WinForms医院挂号系统:三层架构与事务并发实战
简介一份基于C# WinForm的医院挂号管理系统完整源码面向C/S架构初学者、课程设计与毕业设计开发者。系统实现用户、科室、医生管理及门急诊挂号、挂号查询、修改口令、挂号单打印、帮助文档等模块采用MVC分层与存储过程访问数据库密码经MD5加密统计报表以图文形式呈现另支持医生照片上传。资源压缩包共185个文件以64个cs源码、42个jpg图片、19个resx/resources资源文件为主另含3个sql脚本、2个chm帮助文档和exe、dll、配置文件整体约10.03MB。已有1265人学习下载。代码分层明确数据库脚本和可执行程序均包含在内可快速还原项目环境适合用于学习WinFormMVC存储过程MD5加密的综合应用也可作为医院挂号管理系统的功能参考与二次开发基础。1. 一个 C# WinForms 医院挂号管理系统为什么窗口业务还在用它我前段时间接手了一个“跑不动”的老系统打开源码一看就是 C# 语言写的 WinForms 医院挂号管理系统。主窗体左边是科室树中间是医生排班表右边是当日号源池界面谈不上好看但逻辑非常完整门诊挂号、退号、号源锁、排班生成、收费明细都在里面。这种 Windows 桌面程序在医院窗口场景里一点都不稀奇信息科电脑配置不高、内网环境封闭反而最需要这种双击就能跑、不依赖浏览器的客户端。这篇文章适合两类人一类是练手 C# WinForms 的初学者想找一个有真实业务、不是学生作业的完整项目来拆另一类是刚接手老项目、需要快速看懂别人代码并改业务的开发者。读完你不仅能跑起来这个挂号系统还能顺着它的三层架构和核心事务逻辑自己改出一个预约签到流程。2. 技术选型与三层架构一个挂号系统凭什么还跑在 WinForms 上2.1 技术栈画像WinForms 在医疗信息化里不是怀旧很多人一看到 WinForms 就觉得过时但医院挂号这种场景恰恰是它的舒适区。诊室里的电脑通常还在用着老版本 Windows浏览器兼容问题能折磨死人而一个 .NET Framework 的 WinForms 程序只要机器上装了对应运行时双击 exe 就能工作。再加上很多第三方医保接口、读卡器驱动、票据打印机组件本身只提供桌面 SDK用桌面客户端反而省事。这个项目的技术选型我梳理过属于中小医院信息科最常见的组合开发工具Visual Studio打开 .sln 解决方案即可编译。框架.NET Framework 4.x 或同等级环境依赖少部署成本低。语言C#WinForms 窗体 自定义控件。数据库SQL Server存储科室、医生、号源、挂号单等关系型数据。架构三层结构UI 层做界面BLL 层写业务规则DAL 层管数据库访问。说句实在话这个组合没有微服务、没有 Redis、没有消息队列但它稳定。挂号系统的核心诉求是“别在高峰时段挂不了号”一个事务把号源和挂号单同时落库比任何花哨架构都管用。我一般看到这种项目最先看的不是界面而是它怎么组织代码。2.2 解决方案目录UI、BLL、DAL 三层怎么分配职责解压源码后解决方案里的项目划分很干净典型的按“职责分层”而不是按“功能模块”分。目录大致是这样HospitalRegister/ ├── HospitalRegister.UI // WinForms 窗体按钮事件、界面绑定 │ ├── Forms │ │ ├── LoginForm.cs │ │ ├── MainForm.cs │ │ ├── RegisterForm.cs │ │ └── RefundForm.cs ├── HospitalRegister.BLL // 业务逻辑层挂号规则、退号校验 │ ├── ScheduleManager.cs │ ├── RegisterManager.cs │ └── RefundManager.cs ├── HospitalRegister.DAL // 数据访问层SqlHelper、SQL 语句 │ ├── SqlHelper.cs │ ├── DoctorDal.cs │ └── RegistrationDal.cs ├── HospitalRegister.Models // 实体类 │ ├── Doctor.cs │ ├── Schedule.cs │ └── Registration.cs └── HospitalRegister.Common // 公共类常量、扩展方法第一眼可能觉得项目多实际拆开看非常简单。UI 层只负责界面交互比如在 DataGridView 上显示医生列表、用户点击挂号按钮时收集输入它不直接写 SQL。BLL 层处理“能不能挂号”这类规则比如患者是否重复挂号、号源是否已满、退号窗口是否关闭。DAL 层最底层封装了 SqlHelper所有数据库操作都从这里过。这个分层的好处是以后要加一个“预约挂号”功能只需要在 BLL 加一个预约逻辑DAL 加一张表的读写UI 层加一个预约界面不会牵一发动全身。如果你接手的是一个把 SQL 写在窗体按钮事件里的单文件项目再来看这种三层结构血压会稳定很多。2.3 数据表设计科室、医生、排班、挂号单的关系挂号系统要管住的核心其实是“号源”。一张排班表决定某医生某天某个时段放多少个号一张挂号单记录谁占用了哪个号退号再把号还回去。项目里的表结构大致是下面这套我在很多同类项目里看到的也都八九不离十表名中文用途关键字段关系dept科室表dept_id, dept_name医生表的外键来源doctor医生表doctor_id, doctor_name, dept_id, title按科室归属doctor_schedule医生排班表schedule_id, doctor_id, work_date, time_slot, total_limit, booked_count医生一对多排班registration挂号单表reg_no, patient_name, patient_id_card, doctor_id, schedule_id, reg_fee, status号源被占用的记录设计得比较关键的是 doctor_schedule 这张表它同时存了 total_limit 和 booked_count。total_limit 是该时段最大号数比如普通门诊上午放 30 个号booked_count 是已挂出去的数量。每次挂号不是插入一条记录就完事而是先把 booked_count 加一如果加不进去说明号没了这个思路后面核心代码会讲。下面是建表脚本片段字段用 nvarchar 是为了保证中文不乱码日期和时间段还加了一个唯一约束防止同一医生同一时段被重复初始化排班。CREATE TABLE doctor_schedule ( schedule_id INT IDENTITY PRIMARY KEY, doctor_id INT NOT NULL, work_date DATE NOT NULL, time_slot VARCHAR(20) NOT NULL, -- 上午 / 下午 total_limit INT NOT NULL DEFAULT 30, booked_count INT NOT NULL DEFAULT 0, is_active BIT NOT NULL DEFAULT 1, UNIQUE (doctor_id, work_date, time_slot) );我在实际项目里看到过不少把排班做错的版本比如用字符串拼接2025-01-06上午当主键结果遇到改班次就抓瞎。这个表把日期和时间段拆成两个字段查询某天号源、统计某医生月排班都顺手值得抄作业。3. 核心模块拆解一次挂号从号源生成到落库的完整链路3.1 挂号主流程从选中科室到打票中间发生了什么如果只看界面挂号就是一通点击:选科室、选医生、选上午下午、填患者信息、点挂号。但代码里走得是一条完整业务链路。我把它拆成五步加载科室树用户选科室后联动加载该科室医生。选择医生后查询该医生当前日期的号源剩余情况。提交挂号BLL 层先校验患者信息和号源余量。开启数据库事务锁定排班行并将 booked_count 加一。插入挂号单提交事务返回挂号序号并打印小票。这五步里第四步和第五步必须放在同一个事务里。如果先插入挂号单再更新号源可能出现挂号单有了但号源没扣两个窗口同时挂同一个号的情况。下面几节我把三个核心代码段拿出来讲这段逻辑你以后在别的业务里也能复用。3.2 排班生成用一段 C# 循环把一周号源铺满排班功能通常不是手工一条条插入而是由一个“生成周排班”的批量操作完成。核心逻辑是遍历当前周的每一天再把医生每天分成上午、下午两个时段如果数据库里还没有排班记录就插入一条默认号源记录。/// summary /// 为指定医生生成一周排班起始日期为传入的周一 /// /summary public void GenerateWeekSchedule(int doctorId, DateTime weekStart) { string[] timeSlots { 上午, 下午 }; // 周一至周五排班周六周日默认休息 for (int i 0; i 5; i) { DateTime workDate weekStart.AddDays(i); foreach (string slot in timeSlots) { using (SqlConnection conn new SqlConnection(connStr)) { // 先判断是否已存在避免重复点击生成导致主键冲突 string sql IF NOT EXISTS ( SELECT 1 FROM doctor_schedule WHERE doctor_id doctorId AND work_date workDate AND time_slot slot ) INSERT INTO doctor_schedule(doctor_id, work_date, time_slot, total_limit, booked_count) VALUES(doctorId, workDate, slot, 30, 0); SqlCommand cmd new SqlCommand(sql, conn); cmd.Parameters.AddWithValue(doctorId, doctorId); cmd.Parameters.AddWithValue(workDate, workDate); cmd.Parameters.AddWithValue(slot, slot); conn.Open(); cmd.ExecuteNonQuery(); } } } }这里有两个细节要注意。第一用IF NOT EXISTS做幂等判断而不是直接INSERT这样“生成排班”按钮连点几次不会报错也不会重复插入第二day 循环只跑到周五实际项目里周六日上午也会放部分号源你可以把循环条件改成7或者在周末时段设置较少的 total_limit。参数slot用字符串传值会比把“上午/下午”写死进 SQL 语句安全得多也不容易出现中文编码问题。3.3 挂号事务两个窗口同时按提交怎么避免重复挂号挂号窗口不是单机程序同一时间可能有多个窗口操作同一个医生的号源。如果代码先SELECT看剩余号再INSERT挂号单两个窗口同时查到还剩 1 个号就会挂出 2 个号。正确做法是把扣号源和插单放同一个事务并且让扣号源这个UPDATE语句自己判断余量条件。public bool Register(Registration reg, int scheduleId) { using (TransactionScope scope new TransactionScope()) using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); // 条件更新号源只有当前余量小于上限时才允许 booked_count 加一 string updateSql UPDATE doctor_schedule SET booked_count booked_count 1 WHERE schedule_id scheduleId AND booked_count total_limit; SqlCommand updateCmd new SqlCommand(updateSql, conn); updateCmd.Parameters.AddWithValue(scheduleId, scheduleId); // 影响行数为 0说明号源已满整个事务回滚 int affected updateCmd.ExecuteNonQuery(); if (affected 0) { return false; } // 插入挂号单 string insertSql INSERT INTO registration(patient_name, patient_id_card, dept_id, doctor_id, schedule_id, reg_fee, status, create_time) VALUES(patientName, idCard, deptId, doctorId, scheduleId, regFee, 1, GETDATE()); SqlCommand insertCmd new SqlCommand(insertSql, conn); insertCmd.Parameters.AddWithValue(patientName, reg.PatientName); insertCmd.Parameters.AddWithValue(idCard, reg.PatientIdCard); insertCmd.Parameters.AddWithValue(deptId, reg.DeptId); insertCmd.Parameters.AddWithValue(doctorId, reg.DoctorId); insertCmd.Parameters.AddWithValue(scheduleId, scheduleId); insertCmd.Parameters.AddWithValue(regFee, reg.RegFee); insertCmd.ExecuteNonQuery(); // 事务在此真正提交 scope.Complete(); return true; } }这段代码的核心是UPDATE ... WHERE booked_count total_limit它把“查余量”和“占号源”合并成了一条原子 SQL。SQL Server 会在这个UPDATE执行期间锁定对应行第二个窗口的事务只能等第一个提交或回滚这样就天然避免了并发抢号问题。用TransactionScope的好处是它的范围可以包住多条命令即使后面再加一条更新操作只要没有调用Complete()前面所有写操作都会回滚。这里返回false的设计也方便界面直接提示“号源已满”不用靠异常来传递业务状态。3.4 退号回补号源恢复与退费登记要放在同一个事务退号是挂号的反向操作核心不是 delete 挂号单而是要保留一条状态为“已退号”的记录同时把排班表的 booked_count 减一。这样以后统计科室工作量、医生退号率时才有数据可查。贴一段退号核心代码。public bool Refund(long regNo) { using (TransactionScope scope new TransactionScope()) using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); // 先把挂号单状态置为 2表示已退号 string updateRegSql UPDATE registration SET status 2, refund_time GETDATE() WHERE reg_no regNo AND status 1; SqlCommand regCmd new SqlCommand(updateRegSql, conn); regCmd.Parameters.AddWithValue(regNo, regNo); int regAffected regCmd.ExecuteNonQuery(); if (regAffected 0) { return false; // 已经退过的单子不能重复退 } // 回补号源通过子查询找到该单对应的排班 string updateScheSql UPDATE doctor_schedule SET booked_count booked_count - 1 WHERE schedule_id ( SELECT schedule_id FROM registration WHERE reg_no regNo ); SqlCommand scheCmd new SqlCommand(updateScheSql, conn); scheCmd.Parameters.AddWithValue(regNo, regNo); scheCmd.ExecuteNonQuery(); scope.Complete(); return true; } }退号在这个项目里不是单纯的删数据而是“状态流转”这个想法很重要。状态字段status用1表示已挂号2表示已退号查询当日挂号量时过滤status 1查退费报表时统计status 2两种数据从一张表里出口径不会打架。回补号源时用子查询去找排班 id保证了即使 UI 层传错了参数数据库也不会把别的医生号源减掉。4. 运行与避坑连接串、框架版本和并发挂号的三块敲门砖4.1 把下载源码跑起来的三个步骤拿到这个项目后我一般按三步走。第一步确认本机装了 Visual Studio装上“使用 .NET Framework 的桌面开发”工作负载第二步用 SQL Server 执行项目里的数据库脚本建出数据库和表第三步改好连接字符串编译运行。顺序不能反很多新手上来就 F5然后卡在一堆红色的环境错误上。实际动手时最容易忽略的是数据库文件附加方式。项目里如果带了.mdf数据库文件可以直接放到 SQL Server 的 DATA 目录下附加如果只带了sql脚本就用 SQL Server Management Studio 新建一个数据库然后执行脚本。我建议优先用脚本建库因为你能看到每张表的字段注释对后续改动心里有底。连接字符串一般在App.config里就是下面这个结构connectionStrings add nameHospitalDb connectionStringServerlocalhost;DatabaseHospitalRegister;User Idsa;Passwordyour_password; providerNameSystem.Data.SqlClient / /connectionStrings这段配置将SqlHelper.cs里读取的HospitalDb指向了本机 SQL Server。你要是用 Windows 身份认证登录数据库要把User Id和Password这两项改成Integrated SecuritySSPI。这里提一条血泪经验测试环境用sa账号无所谓但如果你要把这个系统部署到真实诊室电脑上务必给程序单独建一个只读和写业务表的数据库账号别把最高权限直接暴露给客户端。4.2 运行时常见问题怎么定位项目跑起来后如果点击挂号没反应八成是事务回滚了。这时候不要先猜业务代码直接在 Visual Studio 里给Register方法第一行下断点单步看影响行数。我之前帮一个人看到问题他点击挂号后界面一直提示“号源已满”但排班表里明明显示剩余号还有很多。最后发现是 SQL 里比较booked_count total_limit时数据库的这两个字段类型不匹配总号数是int、已挂数是varchar隐式转换把条件弄废了所有更新都匹配不到行。这类问题靠肉眼看代码很难看出来拉出数据库实际数据和断点变量比反复改 UI 快得多。4.3 避坑记录新手最容易踩的三类坑下面这几条是我拆这个项目时实际记录下来的问题也是群里经常有人问到的。现象一编译时报错“未能找到类型或命名空间名称”但代码看起来没问题。原因解决方案里多个类库项目目标 .NET 版本不一致比如 BLL 项目是 .NET Framework 4.8UI 项目被写成 .NET Core引用的程序集对不上。解决在解决方案管理器里选中所有项目检查目标框架统一成同一个版本后重新编译。遇上这种问题把整个解决方案的目标框架统一一次能省下一整周的玄学排查时间。现象二窗口里输入中文患者姓名保存到数据库变成乱码。原因connection string 没处理客户端字符集或者建表时用了varchar而不是nvarchar。解决数据库表字段统一用nvarchar插入参数时用SqlDbType.NVarChar。如果你接手的老库里已经是varchar至少在每次传参时加上N例如N张三能临时缓解但根治还是要改表结构。现象三两个窗口同时抢最后一个号后台日志里出现了两条挂号成功记录。原因原代码用了“先查询余量再插入挂号单”的方式两个事务同时读到余量为 1各自插了一条记录。解决改成前面第 3 章那种条件更新写法把booked_count的加一操作和余量判断放进同一条UPDATE语句并且整个操作包在事务里。改完之后再找两台电脑并发做压测基本不会再翻车。第 4 章的排查逻辑总结起来就是环境问题先统一框架版本中文问题优先看字符类型并发问题回到事务与 SQL 原子性上找答案。别急着在 UI 层加各种 if 判断底层数据的一致性兜住了界面就稳了。5. 二次开发实战给窗口挂号加一个“预约 签到”流程这个项目要接真实需求我第一个想到的改造是加“预约”。很多医院现在希望患者先在公众号或电话预约再到窗口签到而不是到了医院才挂号。改造不需要推翻现有代码只需要加一张预约表然后把窗口挂号的入口逻辑改成“先验证预约再走原有挂号事务”。5.1 数据层新增预约表并保持号源统一预约表只需要存预约码、患者信息、排班 id、状态不直接扣号。真实扣号仍然发生在窗口签到那一瞬间这样即使患者预约了没来号源也不会被白白占住。CREATE TABLE appointment ( appoint_no INT IDENTITY PRIMARY KEY, appoint_code VARCHAR(32) NOT NULL, -- 预约流水号 patient_name NVARCHAR(20) NOT NULL, patient_id_card NVARCHAR(18), schedule_id INT NOT NULL, status TINYINT DEFAULT 0, -- 0已预约 1已签到 2已取消 create_time DATETIME DEFAULT GETDATE() );这里有意不加总号数、不加已约数避免出现“预约池”和“挂号池”两套号源。患者在窗口签到时程序从appointment表把状态改成已签到然后复用Register方法完成原来的号源扣减。这样一套逻辑下来报表里统计的还是registration表不会产生两套口径。5.2 改造挂号窗口预约码验证后走原事务界面改造也比较简单在挂号窗体顶部加一个“预约码”输入框患者提供预约码后点击“签到挂号”按钮。代码大致是这样private void btnAppointSign_Click(object sender, EventArgs e) { // 1. 验证预约码并把预约状态从“已预约”改成“已签到” bool valid appointmentDal.CheckAndMark(txtAppointCode.Text.Trim()); if (!valid) { MessageBox.Show(预约码无效或已使用); return; } // 2. 复用原挂号事务真正扣减号源 Registration reg new Registration { PatientName txtPatientName.Text.Trim(), PatientIdCard txtIdCard.Text.Trim(), DoctorId (int)cboDoctor.SelectedValue, RegFee decimal.Parse(txtFee.Text) }; bool ok registerManager.Register(reg, (int)cboSchedule.SelectedValue); if (ok) { MessageBox.Show(挂号成功); } }CheckAndMark里用一条UPDATE appointment SET status 1 WHERE appoint_code code AND status 0做原子标记影响行数为 0 就说明预约码无效或已经用过不用额外加并发锁。患者爽约的问题可以通过一张“预约未签到”的统计报表解决每天早上把昨天status 0的预约记录拉出来给医生看当日实际出诊参考量。这个改造控制在十行代码左右核心是让新业务复用了旧的扣号事务而不是另起炉灶写一套签到逻辑。在那以后我接到任何挂号类的二次开发需求都会先问一句“号源在哪张表、谁来扣减、退号怎么回补”把这三个问题答上来了才动手改代码而不是急着往窗体上拖一个新按钮。这个习惯帮我省过不少返工时间也让我在拆每个老项目时都多留一份心眼希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

伪协议攻击:从ARP欺骗到证书伪造的信任链攻防解析

伪协议攻击:从ARP欺骗到证书伪造的信任链攻防解析

1. 伪协议攻击的本质:被冒名的从来不是协议,而是信任先说句有点反直觉的话:作为安全从业者,我很少把攻击者的核心手法看作“攻击协议本身”。协议就是一套数据交换规则,它本身没有善恶,更不会主动作恶。真正…

📅 2026/10/16 6:52:07
MPU6500六轴IMU驱动:从寄存器配置到互补滤波姿态解算全攻略

MPU6500六轴IMU驱动:从寄存器配置到互补滤波姿态解算全攻略

简介:面向嵌入式开发、机器人、无人机与物联网设备等场景的MPU6500六轴惯性测量单元驱动算法实现,覆盖加速度计与陀螺仪的原始数据读取、校准、融合及姿态解算全流程,可帮助有嵌入式基础的开发者缩短传感器调试周期。资源包整体约730KB&#…

📅 2026/10/16 6:52:07
51单片机智能小车入门:从点亮LED到驱动电机的实战指南

51单片机智能小车入门:从点亮LED到驱动电机的实战指南

简介:本资源是一份面向嵌入式初学者的51单片机智能小车实战入门工程包,聚焦“让小车动起来”这一核心目标,解决硬件驱动与基础运动控制的学习痛点。压缩包共17个文件,包含2个C源文件(motor_control.c、main.c&#xff…

📅 2026/10/16 6:52:07
MORE NEWS

更多资讯

📰

程序员转型AI大模型:机遇、陷阱与实战路线全解析

我自己在工程师圈子里观察到一个现象:最近半年,讨论“程序员转型AI大模型”的人,比讨论框架版本更新的人还多。打开技术社区,永远有人在问“现在转大模型还来得及吗”;朋友圈里,不是晒大模型课程打卡&#…

📰

Git新手5步实操地图:从clone到push的最小闭环

1. 这不是命令行说明书,而是一张你随时能抄作业的Git操作地图“10分钟图文并茂手把手教会你Git常用指令”——看到这个标题,我第一反应不是点开,而是把手机倒扣在桌面上,泡了杯浓茶。为什么?因为过去三年里&#xff0c…

📰

面向过程与面向对象:本质是建模思维的分岔路口

1. 为什么“面向过程”和“面向对象”不是两种写法,而是两种思考方式?刚入行那会儿,我带过几个零基础转行的学员。有位A同学学完C语言函数调用后信心满满,说:“面向过程不就是把任务拆成一个个函数嘛?”接着…

📰

Git高频指令速查手册:10个救命命令手把手教学

1. 这不是Git教程,是“能立刻用起来”的指令速查手册你有没有过这种时刻:刚在团队群里被,说“主干分支被误提交了,快回滚”,你手指悬在键盘上,脑子里却只浮现出git push和git pull两个词?或者写…

📰

给AI装上“心智”:陪伴场景下的五模块状态框架

我一直觉得,聊天陪伴型AI最难的地方不在“会说话”,而在“有状态”。你把市面上一款主流对话模型接上记忆库、加好人设提示词,刚开始聊体验确实不错;可聊上一周,那种“空洞感”很快就浮出来了——同一个问题&#xff0…

📰

OpenCV Python人脸识别实战:从环境搭建到实时检测与识别

1. 从零搭建人脸识别环境:版本选择和安装陷阱先说明一下,这篇文章是一篇我自己从头做过很多遍的实战记录,不是什么理论科普。标题里提到的OpenCV和Python人脸识别,是我在实际项目里用了很久的一条技术路线。它最大的优势就是轻量、…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬