☰
C# WinForm医院挂号管理系统:从排班到退号的完整实现与避坑指南
2026/10/24 6:48:20 网站建设 项目流程

简介:这是一份基于C#语言与WinForm技术栈开发的医院挂号管理系统源码项目,采用C/S架构与MVC分层设计,面向计算机专业学生、.NET初学者及需要课程设计或毕业设计参考的开发者,可用于学习桌面端信息管理系统的完整实现思路。系统功能覆盖用户管理、科室管理、医生管理、门急诊挂号、挂号查询、修改口令、挂号单打印与帮助文档等模块,表示层、业务层与数据访问层职责分明,数据库操作借助存储过程完成,用户密码采用MD5加密,统计信息以图文并茂的报表形式呈现,并支持医生照片上传以便查看详细信息。压缩包共185个文件,约10.03MB,以64个cs源码文件为核心,辅以jpg图片、resx与resources资源文件、sql脚本、exe程序及chm帮助文档等,结构完整便于对照研读。目前已有1267人学习下载,适合作为分层架构、存储过程与报表展示等知识点的实践参考。

1. 从一张排班表说起:这套医院挂号管理系统到底解决什么问题

门诊高峰期最怕什么?不是医生不够,而是号源分配全靠一张 Excel 排班表加两个窗口,患者凌晨排队、护士手工划号、退号靠电话通知,最后数据对不上账。这套用 C# WinForm 做的医院挂号管理系统,瞄准的就是中小型门诊这个真实场景:把科室、医生、排班、号源、挂号、退号、收费、统计串成一条闭环,让窗口人员在一个客户端里完成全部操作。它适合两类人——正在做课程设计或毕业设计、需要一个结构完整且能跑起来的桌面系统的学生,以及要给小型门诊搭一套轻量级挂号工具、不想上重型平台的开发者。整套方案不依赖浏览器和服务器部署,单机或局域网内即可运行,交付门槛低,但业务逻辑一点不含糊。

2. 需求与角色拆解:先把业务边界划清楚再动手

2.1 三类角色与核心用例

做这类系统最容易翻车的地方,是一上来就画界面、连数据库,结果做到一半发现「退号要不要退费」「一个医生能不能跨科室排班」这些规则没定,代码反复推翻。我一般的做法是先把角色和用例钉死,再谈技术。

系统里通常只有三类角色,权限边界清晰:

角色核心用例数据可见范围
挂号员(窗口)查号源、挂号、退号、打印凭条全部科室号源与挂号记录
医生查看本人当日号序、叫号、标记已就诊仅本人排班与挂号
管理员科室/医生/排班维护、号源生成、收费统计全量数据

挂号员是高频操作者,界面响应必须快;医生端只读为主,重点是号序清晰;管理员负责基础数据,操作频率低但一旦出错影响全局,所以基础数据的增删改要加校验和二次确认。把这张表贴在需求文档第一页,后面所有模块设计都围着它转,就不会跑偏。

2.2 号源模型:整个系统的地基

号源是这套系统的核心实体,设计错了后面全乱。我的做法是把「排班」和「号源」拆成两层:排班描述某医生某天上午/下午出诊、限号多少;号源是排班按规则展开后的一个个可挂号位。这样退号、加号、停诊都好处理。

-- 排班表:一个医生一天最多两条(上午/下午) CREATE TABLE Schedule ( ScheduleId INT IDENTITY PRIMARY KEY, DoctorId INT NOT NULL, WorkDate DATE NOT NULL, TimeSlot TINYINT NOT NULL, -- 1=上午 2=下午 TotalQuota INT NOT NULL, -- 限号数 RegFee DECIMAL(8,2) NOT NULL, -- 挂号费 Status TINYINT DEFAULT 1 -- 1正常 0停诊 ); -- 号源表:排班展开后的具体号位 CREATE TABLE RegSlot ( SlotId INT IDENTITY PRIMARY KEY, ScheduleId INT NOT NULL, SerialNo INT NOT NULL, -- 序号 1..N Status TINYINT DEFAULT 0, -- 0可挂 1已挂 2已退 3已就诊 PatientId INT NULL, RegTime DATETIME NULL );

排班和号源分离的好处是:停诊时只需把 Schedule 状态置 0,关联号源批量置为不可挂;退号时只改单个 Slot 的状态,不动排班。SerialNo 保证叫号顺序稳定,不会因为退号出现空号错乱。这里有个血泪经验——号源状态千万别用「删除记录」表示退号,一旦删了就没法统计退号率,也没法追溯,状态位才是正解。

2.3 技术选型:为什么是 WinForm + ADO.NET

有人会问,2024 年了还做 WinForm 是不是过时。放到这个场景里,选型逻辑其实很实在:目标用户是窗口人员和学生答辩环境,不需要跨平台,不需要浏览器兼容,部署一台机器装个 exe 加数据库就能跑。WinForm 拖控件快、事件模型直观、调试成本低,对课程设计来说能在有限周期内做出完整闭环,这比追新技术栈更划算。

数据访问层用 ADO.NET 而不是重型 ORM,原因是挂号业务 SQL 不复杂但要求可控——号源扣减、退号回滚这类操作需要精确控制事务和锁,手写 SQL 反而清楚。连接串放配置文件,方便换机器部署:

<!-- App.config --> <connectionStrings> <add name="HospitalDb" connectionString="Server=.;Database=HospitalReg;User Id=sa;Password=***;" providerName="System.Data.SqlClient" /> </connectionStrings>

数据库选 SQL Server Express 版即可,免费且和 ADO.NET 天然契合。如果答辩环境不方便装 SQL Server,把连接层抽象成接口、换 SQLite 也能跑,这一点在 2.4 会讲怎么留口子。

2.4 分层结构:UI、BLL、DAL 三层怎么切

WinForm 项目最容易写成「窗体里塞满 SQL」,改一个字段要翻十个文件。我一般强制三层:UI 只负责取值和展示,BLL 放业务规则(比如「同一患者同科室当天不能重复挂」),DAL 只管增删改查。层间用实体类传递,不传 DataTable,避免 UI 和数据库结构耦合。

// BLL 层挂号核心逻辑:事务 + 号源校验 public class RegisterService { public RegResult Register(int slotId, int patientId) { using (var conn = DbHelper.CreateConnection()) { conn.Open(); using (var tran = conn.BeginTransaction()) { try { // 行锁防止并发抢同一号 var slot = SlotDal.GetForUpdate(conn, tran, slotId); if (slot == null || slot.Status != 0) return RegResult.Fail("该号源已被占用"); SlotDal.MarkTaken(conn, tran, slotId, patientId); RegRecordDal.Insert(conn, tran, slotId, patientId); tran.Commit(); return RegResult.Ok(slot.SerialNo); } catch (Exception ex) { tran.Rollback(); LogHelper.Error("挂号失败", ex); return RegResult.Fail("系统繁忙,请重试"); } } } } }

这段代码的关键点有三个:一是GetForUpdate在查询时加行锁(SQL Server 用WITH (UPDLOCK, ROWLOCK)),防止两个窗口同时抢最后一个号;二是整个挂号动作放在一个事务里,号源状态和挂号记录要么都成功要么都回滚;三是异常统一捕获并记日志,UI 层只拿到「成功/失败」和提示语,不暴露堆栈。参数上 slotId 是号源主键,patientId 是患者主键,返回结果里带序号方便打印凭条。这套写法在答辩时能讲清楚「并发怎么防」,是加分项。

3. 核心模块实现:挂号、退号、排班与统计怎么落地

3.1 挂号流程:从选科室到出凭条

挂号是主流程,界面通常做成「科室树 → 医生列表 → 日期时段 → 号源网格」四段式。用户点科室,右侧刷新医生;选医生和日期,下方 DataGridView 展示该排班下所有号源,已挂的置灰。双击可挂号源弹出患者信息确认(支持身份证读卡或手输),确认后调 BLL 的 Register 方法,成功后弹出凭条预览。

这里有个交互细节值得说:号源网格刷新要用「差量更新」而不是每次全量重绑,否则高峰期频繁刷新会闪屏。我的做法是挂号成功后只改对应行的 Status 单元格颜色和文本,不重新查库。凭条打印用System.Drawing.Printing的 PrintDocument,模板固定几个字段:科室、医生、日期时段、序号、挂号费、患者姓名。打印失败不能影响挂号结果,所以打印逻辑放在事务提交之后,单独 try-catch。

3.2 退号与费用处理:状态回滚不能只改一个字段

退号看着简单,实际坑最多。合格的做法是:校验该号是否已就诊(已就诊不允许退)、是否在可退时间窗内(比如就诊当日开诊前可退),然后在一个事务里把 Slot 状态从 1 改回 2(已退),同时写一条退号记录,如果涉及退费再写一条收费流水负数。

public RegResult Cancel(int slotId, string reason) { using (var conn = DbHelper.CreateConnection()) { conn.Open(); using (var tran = conn.BeginTransaction()) { var slot = SlotDal.GetForUpdate(conn, tran, slotId); if (slot.Status == 3) return RegResult.Fail("已就诊,不能退号"); if (slot.Status != 1) return RegResult.Fail("该号源状态异常"); SlotDal.SetStatus(conn, tran, slotId, 2); // 已退 CancelLogDal.Insert(conn, tran, slotId, reason); // 退号留痕 if (slot.RegFee > 0) FeeDal.InsertRefund(conn, tran, slotId, slot.RegFee); tran.Commit(); return RegResult.Ok(0); } } }

注意退号后号源状态是 2 而不是回到 0,这样统计时能区分「从未挂过」和「挂了又退」,退号率才算得准。如果业务允许退号后重新放出,再加一步把状态置 0 并清空患者信息,但要在日志里保留原挂号人,方便追溯。

3.3 排班与号源生成:批量操作要防重复

管理员维护排班时,通常是选医生、选日期范围、选时段、填限号,一键生成。这里最容易出的问题是重复生成——同一医生同一天上午点了两次生成,号源翻倍。解决办法是在 Schedule 表上建唯一索引(DoctorId, WorkDate, TimeSlot),插入前先查重,冲突就提示「该时段已排班」。

号源生成用循环插入,数量等于 TotalQuota,SerialNo 从 1 递增。批量插入建议用SqlBulkCopy或拼批量 INSERT,逐条插在限号 50 以内问题不大,但养成批量习惯没坏处。停诊操作要把关联的未挂号源置为不可用,已挂的号源要触发通知(系统内消息或打印停诊公告),这部分在课程设计里可以简化为状态标记加列表提示。

3.4 统计报表:挂号量、退号率、医生工作量

统计模块是答辩时最能体现「系统完整度」的部分。通常做三张报表:按日/科室的挂号量、退号率趋势、医生工作量排名。实现上用一条分组 SQL 出数据,DataGridView 展示,再配一个 Chart 控件画柱状图。

-- 某日各科室挂号量与退号量 SELECT d.DeptName, SUM(CASE WHEN s.Status IN (1,3) THEN 1 ELSE 0 END) AS RegCount, SUM(CASE WHEN s.Status = 2 THEN 1 ELSE 0 END) AS CancelCount FROM RegSlot s JOIN Schedule sc ON s.ScheduleId = sc.ScheduleId JOIN Doctor doc ON sc.DoctorId = doc.DoctorId JOIN Department d ON doc.DeptId = d.DeptId WHERE sc.WorkDate = @date GROUP BY d.DeptName;

参数 @date 由日期控件传入,统计口径要写进文档:RegCount 含已挂和已就诊,CancelCount 只算已退。口径不写清楚,答辩老师一问「你这个数怎么来的」就答不上来。

4. 避坑与常见问题:这些坑我替你踩过了

4.1 并发挂号导致超卖

现象:两个窗口同时点最后一个号,都提示成功,实际卖出两个。原因:查询和更新之间没有锁,两个事务都读到 Status=0。解决:查询号源时加UPDLOCK, ROWLOCK,把「读—判—改」放进同一事务,必要时对 Schedule 行也加锁。测试时开两个客户端同时点,能稳定复现才算修好。

4.2 退号后统计数字对不上

现象:报表里挂号量比实际少,退号率算出来是负数。原因:退号时直接删了 Slot 记录,或者状态位用错。解决:坚持状态位方案,退号置 2 不删记录,统计 SQL 口径统一在文档里定义,所有报表共用同一段 WHERE 条件。

4.3 窗体卡死与跨线程更新

现象:点挂号后界面白屏几秒,或者后台线程更新 DataGridView 抛异常。原因:数据库操作放在 UI 线程,或者子线程直接碰控件。解决:耗时操作放Task.Run,回 UI 用Invoke/BeginInvoke;简单场景也可以先禁用按钮加等待光标,但根治还是异步。WinForm 的跨线程检查是玄学重灾区,CheckForIllegalCrossThreadCalls = false能压住异常但压不住问题,别用。

4.4 连接串写死导致换机部署失败

现象:在自己电脑跑得好好的,拷到答辩机器上连不上库。原因:连接串硬编码在代码里,或者用了 Windows 身份验证但目标机账号不同。解决:连接串放 App.config,部署时只改配置文件;数据库脚本单独提供,初始化时建库建表插基础数据,别指望对方手工建。

4.5 打印凭条格式错位

现象:预览正常,实际打印偏移或截断。原因:没按打印机实际纸张尺寸设置PaperSize,或者用了固定坐标没考虑 DPI。解决:打印前设置自定义纸张(如 80mm 小票),坐标用e.Graphics.DpiX换算,打印前先出预览让用户确认。

5. 验收与答辩:怎么证明这套系统真的能用

验收这件事,我一般分两步走:先自测跑通业务闭环,再准备答辩话术。自测清单按角色走一遍——管理员建科室、建医生、排一周班、生成号源;挂号员挂三个号、退一个、再挂一个;医生端看号序、标记就诊;最后打开统计看数字是否自洽。每一步都截图存档,答辩时按流程演示,比口头讲十遍都管用。

答辩时老师最爱问的三个问题,提前准备好答案:一是「并发怎么处理」,答事务加行锁,能现场开两个客户端演示最好;二是「数据一致性怎么保证」,答号源状态和挂号记录同事务提交,退号留痕不删记录;三是「你的系统跟真实医院差在哪」,坦诚说清边界——本系统面向中小门诊,不含医保结算和线上支付,这是范围控制不是缺陷。把「不做什么」讲清楚,反而显得你想过。

演示节奏上有个技巧:别从登录页开始慢慢点,提前把基础数据准备好,直接进挂号主界面演示核心流程,统计报表放最后压轴。演示前把数据库备份一份,万一现场点崩了能快速恢复,这是后悔药。

提示:答辩机器上提前装好 .NET Framework 对应版本和 SQL Server Express,连接串改成目标机实例名,演示前完整跑一遍,别到现场才发现环境不对。

从那以后我每次交这类桌面系统,都强制自己按「角色—用例—数据—异常」四步走一遍自测,任何一步没过就不算完成。希望这套结构和踩坑记录能帮到你,拿去改改就能变成你自己的方案。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询