院掛號(hào)管理系統(tǒng):從設(shè)計(jì)到實(shí)現(xiàn),解決門診掛號(hào)崩潰)
簡介這是一份基于C#語言與WinForm技術(shù)棧開發(fā)的醫(yī)院掛號(hào)管理系統(tǒng)源碼采用C/S架構(gòu)與MVC分層設(shè)計(jì)面向計(jì)算機(jī)專業(yè)學(xué)生、.NET初學(xué)者及需要課程設(shè)計(jì)或畢業(yè)設(shè)計(jì)參考的開發(fā)者可用于學(xué)習(xí)桌面端信息管理系統(tǒng)的完整實(shí)現(xiàn)思路。壓縮包共185個(gè)文件約10.03MB以64個(gè)cs源碼文件為核心輔以resx與resources資源文件、jpg醫(yī)生照片素材、sql建庫腳本、chm幫助文檔及exe、dll等可執(zhí)行與依賴文件結(jié)構(gòu)較為完整。系統(tǒng)涵蓋用戶管理、科室管理、醫(yī)生管理、門急診掛號(hào)、掛號(hào)查詢、修改口令、掛號(hào)單打印與幫助文檔等模塊表示層、業(yè)務(wù)層、數(shù)據(jù)訪問層分離清晰數(shù)據(jù)庫操作借助存儲(chǔ)過程完成用戶密碼采用MD5加密統(tǒng)計(jì)信息以報(bào)表圖文形式呈現(xiàn)并支持醫(yī)生照片上傳。目前已有1267人學(xué)習(xí)下載適合作為分層架構(gòu)與WinForm界面開發(fā)的實(shí)戰(zhàn)參考。1. 從一次門診高峰的掛號(hào)崩潰說起某高校附屬門診在早高峰時(shí)段做過一次內(nèi)部演練三個(gè)掛號(hào)窗口同時(shí)開放人工登記、找零、核對(duì)醫(yī)保信息平均每位患者耗時(shí)接近四分鐘隊(duì)伍很快排到走廊盡頭。問題不在于人手不夠而在于信息流是斷的——號(hào)源余量靠喊、患者信息靠手寫、退號(hào)改號(hào)靠翻本子。這套用 C# WinForms 做的醫(yī)院掛號(hào)管理系統(tǒng)要解決的就是把這條斷掉的信息流接起來號(hào)源集中管理、患者建檔、掛號(hào)退號(hào)改號(hào)、醫(yī)生排班、收費(fèi)統(tǒng)計(jì)全部收進(jìn)一個(gè)桌面端程序里。它適合兩類人正在做課程設(shè)計(jì)或畢業(yè)設(shè)計(jì)、需要一個(gè)結(jié)構(gòu)完整且能跑起來的桌面信息系統(tǒng)的學(xué)生以及需要一套可交付、可二次修改的門診掛號(hào)原型的從業(yè)者。下面按需求、設(shè)計(jì)、實(shí)現(xiàn)、避坑、驗(yàn)收的順序把這份方案拆開講清楚。2. 需求與選型為什么是 WinForms 而不是 Web2.1 先把業(yè)務(wù)邊界劃清楚掛號(hào)系統(tǒng)的功能看起來簡單真正落到代碼里邊界必須先定死否則做到一半會(huì)發(fā)現(xiàn)表結(jié)構(gòu)撐不住。我一般會(huì)把需求分成三層核心業(yè)務(wù)、支撐數(shù)據(jù)、輔助功能。核心業(yè)務(wù)是掛號(hào)、退號(hào)、改號(hào)、收費(fèi)支撐數(shù)據(jù)是患者檔案、醫(yī)生信息、科室與排班、號(hào)源池輔助功能是查詢統(tǒng)計(jì)、用戶權(quán)限、日志。三層之外的東西比如在線支付、短信通知、醫(yī)保接口對(duì)接一律劃到范圍外課程設(shè)計(jì)階段不要碰碰了就是給自己挖坑。號(hào)源是這套系統(tǒng)的核心資源必須明確它的粒度。通常的做法是按「科室 醫(yī)生 日期 時(shí)段」生成號(hào)源記錄每條記錄帶總號(hào)數(shù)、已掛數(shù)、掛號(hào)費(fèi)。這樣退號(hào)時(shí)只需把已掛數(shù)減一改號(hào)時(shí)先退后掛邏輯閉環(huán)。如果把號(hào)源做成一個(gè)整數(shù)計(jì)數(shù)器掛在醫(yī)生身上跨天、跨時(shí)段就會(huì)亂這是血淚經(jīng)驗(yàn)?;颊邫n案要區(qū)分「首次建檔」和「復(fù)診」用身份證號(hào)或就診卡號(hào)做唯一鍵。醫(yī)生信息要綁定科室和職稱職稱影響掛號(hào)費(fèi)。排班表是號(hào)源生成的上游先有排班才有號(hào)源這個(gè)依賴順序在數(shù)據(jù)庫設(shè)計(jì)時(shí)就要體現(xiàn)出來。2.2 技術(shù)選型的取舍邏輯選 WinForms 不是因?yàn)樗冗M(jìn)而是因?yàn)樗ヅ浣桓秷?chǎng)景。桌面端程序部署簡單雙擊即用不需要配服務(wù)器、配數(shù)據(jù)庫連接池、配前端構(gòu)建工具對(duì)于單機(jī)或局域網(wǎng)內(nèi)幾臺(tái)機(jī)器的門診場(chǎng)景SQL Server Express 或 SQLite 完全夠用。C# 的強(qiáng)類型和 Visual Studio 的設(shè)計(jì)器讓界面拖拽加事件綁定的開發(fā)效率很高學(xué)生能在有限周期內(nèi)做出可演示的成品。數(shù)據(jù)訪問層有兩種常見做法直接拼 SQL 用SqlCommand或者用輕量 ORM。課程設(shè)計(jì)我建議前者因?yàn)槊恳痪?SQL 都看得見答辯時(shí)能講清楚如果追求代碼整潔可以用 Dapper 這類輕量映射但不要上 Entity Framework 的 Code First 遷移配置成本會(huì)把時(shí)間吃掉。數(shù)據(jù)庫選 SQL Server 的話連接字符串寫在配置文件里方便換機(jī)器時(shí)改選 SQLite 則整個(gè)庫就是一個(gè)文件拷貝即遷移適合演示。界面層用多窗體加 MDC 容器主窗體做導(dǎo)航各業(yè)務(wù)窗體獨(dú)立。不要把所有功能塞進(jìn)一個(gè)窗體用 TabControl 切換后期改一個(gè)模塊會(huì)牽動(dòng)全局。權(quán)限控制用簡單的角色表加菜單可見性判斷管理員看全部掛號(hào)員只看掛號(hào)與收費(fèi)醫(yī)生只看自己的排班和就診列表。提示技術(shù)選型要在開題時(shí)就寫進(jìn)文檔答辯時(shí)被問「為什么不用 Web」是高頻問題答案要落在部署成本、開發(fā)周期和場(chǎng)景匹配上而不是「我不會(huì)」。3. 總體設(shè)計(jì)四層結(jié)構(gòu)與六張核心表3.1 分層結(jié)構(gòu)與模塊職責(zé)系統(tǒng)按四層組織界面層、業(yè)務(wù)邏輯層、數(shù)據(jù)訪問層、數(shù)據(jù)存儲(chǔ)層。界面層只負(fù)責(zé)收集輸入和展示結(jié)果不做任何計(jì)算業(yè)務(wù)邏輯層承載掛號(hào)規(guī)則、號(hào)源扣減、費(fèi)用計(jì)算數(shù)據(jù)訪問層封裝增刪改查數(shù)據(jù)存儲(chǔ)層是數(shù)據(jù)庫。這樣分層的好處是掛號(hào)規(guī)則變化時(shí)只改業(yè)務(wù)層界面不用動(dòng)。模塊劃分成六個(gè)登錄與權(quán)限、患者管理、醫(yī)生與排班管理、號(hào)源與掛號(hào)管理、收費(fèi)與退費(fèi)、查詢統(tǒng)計(jì)。每個(gè)模塊對(duì)應(yīng)一組窗體和一個(gè)業(yè)務(wù)類。模塊之間的調(diào)用走業(yè)務(wù)層界面層不直接跨模塊訪問數(shù)據(jù)庫這條紀(jì)律能避免后期出現(xiàn)「改一處崩三處」的連鎖反應(yīng)。下面這張表是核心數(shù)據(jù)表的設(shè)計(jì)字段名和類型可以直接照搬表名關(guān)鍵字段說明PatientPatientId, Name, IdCard, Phone, CardNo患者檔案IdCard 唯一DoctorDoctorId, Name, DeptId, Title, RegFee醫(yī)生信息RegFee 隨職稱ScheduleScheduleId, DoctorId, WorkDate, TimeSlot, MaxCount排班決定號(hào)源上限RegSourceSourceId, ScheduleId, Total, Booked, Fee號(hào)源池Booked 實(shí)時(shí)扣減RegistrationRegId, PatientId, SourceId, RegTime, Status, Fee掛號(hào)記錄Status 標(biāo)記有效/已退UsersUserId, UserName, Password, Role登錄與角色3.2 號(hào)源扣減的事務(wù)設(shè)計(jì)掛號(hào)最怕的是超掛兩個(gè)人同時(shí)搶最后一個(gè)號(hào)。單機(jī)程序并發(fā)低但局域網(wǎng)多終端就會(huì)有競爭。正確做法是把「查余量」和「扣減」放進(jìn)同一個(gè)事務(wù)并且用條件更新防止負(fù)數(shù)-- 掛號(hào)時(shí)扣減號(hào)源條件更新保證不會(huì)超掛 UPDATE RegSource SET Booked Booked 1 WHERE SourceId SourceId AND Booked Total; -- 關(guān)鍵余量為 0 時(shí)更新影響行數(shù)為 0 -- 判斷影響行數(shù)為 0 說明號(hào)已掛滿回滾并提示這段 SQL 的要點(diǎn)在AND Booked Total這個(gè)條件。數(shù)據(jù)庫執(zhí)行更新時(shí)會(huì)加行鎖兩個(gè)并發(fā)請(qǐng)求只有一個(gè)能成功另一個(gè)影響行數(shù)為 0業(yè)務(wù)層據(jù)此提示「該號(hào)源已滿」。比起先SELECT再判斷再UPDATE的寫法這種條件更新把判斷和寫入合成一個(gè)原子操作是防超掛最省事的手段。參數(shù)SourceId來自界面選中的號(hào)源Total和Booked都是整型不要用浮點(diǎn)。退號(hào)則是反向操作把Booked減一同時(shí)把掛號(hào)記錄的Status改為已退兩個(gè)動(dòng)作也要在同一事務(wù)里。改號(hào)按「先退后掛」處理先釋放原號(hào)源再走一次掛號(hào)流程任何一步失敗就整體回滾。事務(wù)邊界用SqlTransaction顯式控制不要依賴自動(dòng)提交。3.3 界面導(dǎo)航與狀態(tài)管理主窗體用菜單欄加側(cè)邊導(dǎo)航登錄成功后根據(jù)角色動(dòng)態(tài)顯示菜單項(xiàng)。窗體之間傳值用構(gòu)造函數(shù)參數(shù)或公共屬性不要用全局靜態(tài)變量靜態(tài)變量在多次打開窗體時(shí)會(huì)殘留舊數(shù)據(jù)是典型的翻車點(diǎn)。掛號(hào)窗體的流程是選科室 → 選醫(yī)生 → 選日期時(shí)段 → 顯示余量 → 選患者或新建檔→ 確認(rèn)掛號(hào)。每一步的選中項(xiàng)存在窗體私有字段里確認(rèn)時(shí)一次性提交業(yè)務(wù)層。4. 關(guān)鍵模塊實(shí)現(xiàn)從登錄到收費(fèi)的代碼骨架4.1 登錄與權(quán)限校驗(yàn)登錄模塊要做三件事驗(yàn)證用戶名密碼、加載角色、按角色控制菜單。密碼不要明文存用 SHA256 加鹽哈希后存庫登錄時(shí)同樣計(jì)算再比對(duì)。下面是一個(gè)可用的校驗(yàn)方法// 登錄校驗(yàn)返回角色失敗返回 null public string Login(string userName, string password) { string sql SELECT Password, Salt, Role FROM Users WHERE UserName name; using (var conn new SqlConnection(connStr)) using (var cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(name, userName); conn.Open(); using (var reader cmd.ExecuteReader()) { if (!reader.Read()) return null; // 用戶不存在 string dbPwd reader.GetString(0); string salt reader.GetString(1); string role reader.GetString(2); // 用相同鹽值計(jì)算哈希后比對(duì) return ComputeHash(password, salt) dbPwd ? role : null; } } }ComputeHash把密碼和鹽拼接后做 SHA256輸出十六進(jìn)制字符串。鹽值在注冊(cè)時(shí)隨機(jī)生成每個(gè)用戶不同。這段代碼的關(guān)鍵點(diǎn)是參數(shù)化查詢AddWithValue防住了 SQL 注入返回 null 統(tǒng)一表示失敗調(diào)用方據(jù)此提示不區(qū)分「用戶不存在」和「密碼錯(cuò)誤」避免被枚舉賬號(hào)。角色字符串直接用于主窗體菜單的可見性判斷管理員顯示全部掛號(hào)員隱藏醫(yī)生管理醫(yī)生隱藏收費(fèi)模塊。4.2 掛號(hào)業(yè)務(wù)的事務(wù)封裝掛號(hào)業(yè)務(wù)層的方法接收患者 ID 和號(hào)源 ID內(nèi)部完成扣減號(hào)源、寫入掛號(hào)記錄、返回結(jié)果三步。核心代碼如下// 掛號(hào)扣號(hào)源 寫記錄同一事務(wù) public bool Register(int patientId, int sourceId, out string msg) { using (var conn new SqlConnection(connStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { try { // 第一步條件扣減號(hào)源 string upd UPDATE RegSource SET Booked Booked 1 WHERE SourceId sid AND Booked Total; using (var cmd new SqlCommand(upd, conn, tran)) { cmd.Parameters.AddWithValue(sid, sourceId); if (cmd.ExecuteNonQuery() 0) { tran.Rollback(); msg 該號(hào)源已掛滿; return false; } } // 第二步寫入掛號(hào)記錄 string ins INSERT INTO Registration (PatientId, SourceId, RegTime, Status, Fee) SELECT pid, SourceId, GETDATE(), 有效, Fee FROM RegSource WHERE SourceId sid; using (var cmd new SqlCommand(ins, conn, tran)) { cmd.Parameters.AddWithValue(pid, patientId); cmd.Parameters.AddWithValue(sid, sourceId); cmd.ExecuteNonQuery(); } tran.Commit(); msg 掛號(hào)成功; return true; } catch (Exception ex) { tran.Rollback(); msg 掛號(hào)失敗 ex.Message; return false; } } } }兩個(gè) SQL 語句共用同一個(gè)SqlTransaction任何一步拋異常都整體回滾號(hào)源不會(huì)出現(xiàn)「扣了但沒記錄」的臟狀態(tài)。Fee從號(hào)源表帶過來保證收費(fèi)金額和號(hào)源定義一致不在代碼里硬編碼。out參數(shù)把提示信息帶回界面層界面只負(fù)責(zé)彈窗不參與業(yè)務(wù)判斷。退號(hào)方法結(jié)構(gòu)相同把UPDATE改成Booked - 1并加Booked 0條件同時(shí)更新掛號(hào)記錄狀態(tài)。4.3 收費(fèi)與統(tǒng)計(jì)查詢收費(fèi)模塊按掛號(hào)記錄匯總支持按日期、科室、醫(yī)生三個(gè)維度篩選。統(tǒng)計(jì)用一條分組查詢搞定避免在代碼里循環(huán)累加-- 按科室統(tǒng)計(jì)某日掛號(hào)量與收入 SELECT d.DeptName, COUNT(r.RegId) AS 掛號(hào)量, SUM(r.Fee) AS 收入合計(jì) FROM Registration r JOIN RegSource s ON r.SourceId s.SourceId 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 r.Status 有效 AND CAST(r.RegTime AS DATE) date GROUP BY d.DeptName ORDER BY 掛號(hào)量 DESC;查詢結(jié)果直接綁定到DataGridView列名用中文別名界面不用再做映射。date參數(shù)由日期選擇器傳入CAST(... AS DATE)去掉時(shí)間部分保證當(dāng)天記錄全部命中。統(tǒng)計(jì)模塊最容易出的問題是把已退號(hào)的記錄也算進(jìn)去所以Status 有效這個(gè)條件不能漏。如果要做月度報(bào)表把日期條件換成范圍查詢其余結(jié)構(gòu)不變。5. 避坑與排查五個(gè)最容易翻車的地方5.1 號(hào)源顯示有余量但掛號(hào)提示已滿現(xiàn)象是界面上顯示還剩 1 個(gè)號(hào)點(diǎn)確認(rèn)卻提示掛滿。原因通常是界面加載余量后沒有刷新或者余量計(jì)算用了緩存值。解決方法是每次打開掛號(hào)窗體時(shí)重新查詢號(hào)源確認(rèn)掛號(hào)時(shí)以數(shù)據(jù)庫條件更新的結(jié)果為準(zhǔn)界面顯示只作參考。更徹底的做法是掛號(hào)成功后立即刷新當(dāng)前號(hào)源列表讓用戶看到實(shí)時(shí)余量。5.2 退號(hào)后號(hào)源沒有回補(bǔ)現(xiàn)象是退了號(hào)但余量沒變號(hào)白白浪費(fèi)。原因多半是退號(hào)只改了掛號(hào)記錄狀態(tài)忘了更新RegSource.Booked或者兩個(gè)更新不在同一事務(wù)里一個(gè)成功一個(gè)失敗。解決方法是把退號(hào)寫成「更新記錄狀態(tài) 號(hào)源減一」的事務(wù)并在測(cè)試時(shí)專門驗(yàn)證「掛滿 → 退一個(gè) → 再掛一個(gè)」這條鏈路。5.3 多臺(tái)機(jī)器同時(shí)掛號(hào)出現(xiàn)重復(fù)記錄現(xiàn)象是同一患者同一時(shí)段出現(xiàn)兩條有效掛號(hào)。原因是程序沒有做重復(fù)校驗(yàn)或者校驗(yàn)和寫入之間有間隙。解決方法是在業(yè)務(wù)層先查該患者在該時(shí)段是否已有有效記錄再執(zhí)行掛號(hào)數(shù)據(jù)庫層可以加唯一索引兜底索引建在(PatientId, SourceId, Status)上狀態(tài)為有效時(shí)唯一。注意唯一索引和軟刪除的配合退號(hào)后狀態(tài)變了索引自然放行。5.4 窗體重復(fù)打開導(dǎo)致數(shù)據(jù)錯(cuò)亂現(xiàn)象是同一個(gè)掛號(hào)窗體開了好幾個(gè)各自顯示不同數(shù)據(jù)操作后互相覆蓋。原因是用了Show()而不是ShowDialog()或者沒有判斷窗體是否已打開。解決方法是業(yè)務(wù)窗體統(tǒng)一用模態(tài)打開主窗體持有子窗體引用打開前判斷是否已存在存在則激活而不是新建。全局靜態(tài)變量傳參也要一并清理改成構(gòu)造函數(shù)傳值。5.5 連接字符串寫死導(dǎo)致?lián)Q機(jī)即崩現(xiàn)象是程序在自己電腦上跑得好好的拷到答辯教室就打不開數(shù)據(jù)庫。原因是連接字符串硬編碼在代碼里指向了本機(jī)實(shí)例名。解決方法是把連接字符串放進(jìn)App.config用ConfigurationManager讀取部署時(shí)只改配置文件。如果用 SQLite數(shù)據(jù)庫文件路徑用相對(duì)路徑隨程序目錄走拷過去就能用。6. 驗(yàn)收與答辯怎么證明這套系統(tǒng)真的能用驗(yàn)收不是把功能點(diǎn)念一遍而是用一條完整的業(yè)務(wù)鏈路證明系統(tǒng)閉環(huán)。我一般會(huì)準(zhǔn)備一條「黃金路徑」新建患者檔案 → 選科室醫(yī)生 → 查號(hào)源余量 → 掛號(hào)成功 → 收費(fèi)記錄出現(xiàn) → 退號(hào) → 余量回補(bǔ) → 再掛同一號(hào)成功。這條路徑走通說明核心業(yè)務(wù)的事務(wù)、狀態(tài)流轉(zhuǎn)、數(shù)據(jù)一致性都成立。答辯時(shí)先演示這條路徑再打開數(shù)據(jù)庫表展示記錄變化比講十頁 PPT 有說服力。驗(yàn)收標(biāo)準(zhǔn)建議列成可勾選的清單下面這張表可以直接放進(jìn)文檔驗(yàn)收項(xiàng)通過標(biāo)準(zhǔn)驗(yàn)證方式登錄權(quán)限三種角色菜單不同分別登錄截圖掛號(hào)閉環(huán)掛號(hào)后余量減一、記錄生成黃金路徑演示防超掛余量為 0 時(shí)掛號(hào)被拒掛滿后連續(xù)點(diǎn)擊退號(hào)回補(bǔ)退號(hào)后余量加一退號(hào)后查庫統(tǒng)計(jì)準(zhǔn)確收入合計(jì)與記錄一致手工核對(duì)當(dāng)日數(shù)據(jù)換機(jī)部署改配置后可直接運(yùn)行換一臺(tái)機(jī)器啟動(dòng)答辯被問得最多的是「并發(fā)怎么處理」和「數(shù)據(jù)一致性怎么保證」答案就落在第 3、4 章的條件更新和事務(wù)上把那段 SQL 講清楚即可。另一個(gè)高頻問題是「為什么不用 Web」回到第 2 章的選型邏輯講部署成本和場(chǎng)景匹配。演示時(shí)提前把測(cè)試數(shù)據(jù)準(zhǔn)備好不要現(xiàn)場(chǎng)錄入大量數(shù)據(jù)時(shí)間不夠。有個(gè)習(xí)慣我每次都強(qiáng)制走一遍答辯前一天把程序拷到一臺(tái)沒裝過開發(fā)環(huán)境的機(jī)器上從改配置到跑通黃金路徑完整走一次。有一次就是這一步發(fā)現(xiàn)數(shù)據(jù)庫文件路徑寫成了絕對(duì)路徑當(dāng)場(chǎng)改掉才沒在答辯時(shí)翻車。從那以后我每次交付前都強(qiáng)制走一遍換機(jī)驗(yàn)證希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取