院管理系統(tǒng)源碼解析:掛號門診藥房住院全流程與數(shù)據(jù)庫設(shè)計(jì))
簡介這是一套基于Java開發(fā)的醫(yī)院管理系統(tǒng)完整源碼與數(shù)據(jù)庫面向醫(yī)療信息化方向的Java開發(fā)者、課程設(shè)計(jì)或畢業(yè)設(shè)計(jì)學(xué)生以及需要了解醫(yī)療業(yè)務(wù)邏輯的技術(shù)人員。系統(tǒng)覆蓋掛號、門診、藥房、住院等核心模塊并涉及權(quán)限控制、接口集成與系統(tǒng)測試等工程實(shí)踐可作為學(xué)習(xí)企業(yè)級項(xiàng)目架構(gòu)的參考。壓縮包共1236個文件約8.91MB以791個java源碼為主體輔以106個xml配置、84個js與37個html前端頁面另有sql腳本、yml與properties配置、dockerfile部署文件及md說明文檔結(jié)構(gòu)完整、層次清晰。目前已有681人學(xué)習(xí)下載。通過閱讀源碼讀者可掌握Spring Boot與MyBatis等框架的落地方式、關(guān)系型數(shù)據(jù)庫表結(jié)構(gòu)設(shè)計(jì)、掛號并發(fā)控制、電子病歷與藥品庫存管理等實(shí)現(xiàn)思路并借鑒其分層目錄與接口設(shè)計(jì)快速搭建自己的醫(yī)療信息化項(xiàng)目或完成二次開發(fā)。1. 從一份 Java 醫(yī)院管理系統(tǒng)源碼說起掛號、門診、藥房、住院到底怎么串起來很多做 Java 課程設(shè)計(jì)或畢業(yè)設(shè)計(jì)的同學(xué)拿到「醫(yī)院管理系統(tǒng)」這個題目時第一反應(yīng)是建幾張表、寫幾個增刪改查就交差。但真正跑過一套完整源碼的人會發(fā)現(xiàn)掛號、門診、藥房、住院這四條業(yè)務(wù)線并不是四個獨(dú)立模塊而是圍繞「患者一次就診」這條主線互相咬合的。掛號產(chǎn)生就診記錄門診醫(yī)生開處方處方流轉(zhuǎn)到藥房扣庫存需要住院則從門診轉(zhuǎn)住院登記出院時又要回寫費(fèi)用和床位狀態(tài)。任何一環(huán)的狀態(tài)沒對齊整個流程就會卡死。這份 Java 醫(yī)院管理系統(tǒng)源碼加數(shù)據(jù)庫覆蓋的正是這條完整鏈路。技術(shù)棧是典型的 Java Web 組合后端用 Servlet/JSP 或 Spring 系框架數(shù)據(jù)庫用 MySQL前端是 JSP 加少量 JavaScript。它適合三類人一是要做課程設(shè)計(jì)、需要一套能跑通全流程參考實(shí)現(xiàn)的學(xué)生二是想拿一個真實(shí)業(yè)務(wù)系統(tǒng)練手?jǐn)?shù)據(jù)庫設(shè)計(jì)、事務(wù)和狀態(tài)機(jī)的開發(fā)者三是面試前想找一個能講清楚「業(yè)務(wù)閉環(huán)」的項(xiàng)目來充實(shí)簡歷的人。下面從數(shù)據(jù)庫設(shè)計(jì)講到接口實(shí)現(xiàn)再到并發(fā)掛號和排錯把這份源碼拆開看。2. 醫(yī)院管理系統(tǒng)數(shù)據(jù)庫表結(jié)構(gòu)與掛號門診核心表設(shè)計(jì)一套醫(yī)院管理系統(tǒng)的質(zhì)量八成取決于數(shù)據(jù)庫設(shè)計(jì)。源碼里表不少但真正決定業(yè)務(wù)能否跑通的是掛號、門診、藥房、住院這四組核心表以及它們之間的外鍵和狀態(tài)字段。2.1 患者、醫(yī)生、科室三張基礎(chǔ)表先看基礎(chǔ)主數(shù)據(jù)。患者表存的是就診人信息醫(yī)生表存的是坐診醫(yī)生科室表是掛號時的分類維度。常見做法是把醫(yī)生和科室用外鍵關(guān)聯(lián)掛號時先選科室再選醫(yī)生。-- 患者表一次建檔多次就診復(fù)用 CREATE TABLE patient ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL COMMENT 姓名, id_card VARCHAR(18) NOT NULL UNIQUE COMMENT 身份證號唯一, phone VARCHAR(15) COMMENT 聯(lián)系電話, gender TINYINT DEFAULT 0 COMMENT 0未知 1男 2女, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 科室表 CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL COMMENT 科室名稱如內(nèi)科、外科 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 醫(yī)生表一個醫(yī)生歸屬一個科室 CREATE TABLE doctor ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL, dept_id INT NOT NULL COMMENT 所屬科室, title VARCHAR(16) COMMENT 職稱, register_fee DECIMAL(8,2) DEFAULT 0 COMMENT 掛號費(fèi), FOREIGN KEY (dept_id) REFERENCES department(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;id_card上加唯一索引是關(guān)鍵它保證同一個患者不會因?yàn)橹貜?fù)建檔產(chǎn)生多條記錄后續(xù)查歷史就診才有依據(jù)。register_fee放在醫(yī)生表而不是科室表是因?yàn)橥豢剖也煌毞Q的掛號費(fèi)不同這是真實(shí)醫(yī)院的常見規(guī)則。2.2 掛號表與門診記錄表的狀態(tài)字段掛號表和門診表是整條主線的起點(diǎn)。掛號表記錄「誰掛了哪個醫(yī)生的號、什么狀態(tài)」門診表記錄「這次就診醫(yī)生寫了什么」。-- 掛號表 CREATE TABLE registration ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id BIGINT NOT NULL, doctor_id BIGINT NOT NULL, reg_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 0 COMMENT 0待就診 1已就診 2已取消, visit_no VARCHAR(32) UNIQUE COMMENT 就診流水號, FOREIGN KEY (patient_id) REFERENCES patient(id), FOREIGN KEY (doctor_id) REFERENCES doctor(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 門診記錄表一次掛號對應(yīng)一條門診記錄 CREATE TABLE outpatient_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, registration_id BIGINT NOT NULL UNIQUE COMMENT 關(guān)聯(lián)掛號, diagnosis VARCHAR(255) COMMENT 診斷結(jié)果, advice TEXT COMMENT 醫(yī)囑, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (registration_id) REFERENCES registration(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status字段是這套系統(tǒng)的靈魂。掛號后是 0醫(yī)生接診后置 1患者取消置 2。門診記錄表用registration_id做唯一外鍵保證一次掛號只能有一條門診記錄避免重復(fù)開單。visit_no用唯一約束生成就診流水號住院、藥房都靠它串聯(lián)。2.3 藥房庫存表與處方明細(xì)表門診開完處方藥房要扣庫存。這里最容易出問題的是「處方明細(xì)」和「藥品庫存」的對應(yīng)關(guān)系。表名作用關(guān)鍵字段關(guān)聯(lián)medicine藥品字典id, name, stock, price被處方明細(xì)引用prescription處方主表id, record_id, status關(guān)聯(lián)門診記錄prescription_item處方明細(xì)prescription_id, medicine_id, num關(guān)聯(lián)藥品CREATE TABLE medicine ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, stock INT DEFAULT 0 COMMENT 庫存數(shù)量, price DECIMAL(8,2) DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE prescription_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, prescription_id BIGINT NOT NULL, medicine_id BIGINT NOT NULL, num INT NOT NULL COMMENT 開藥數(shù)量, FOREIGN KEY (prescription_id) REFERENCES prescription(id), FOREIGN KEY (medicine_id) REFERENCES medicine(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;庫存扣減必須和處方寫入放在同一個事務(wù)里否則會出現(xiàn)「處方開了但庫存沒扣」或「庫存扣了但處方?jīng)]存」的臟數(shù)據(jù)。這是后面第 4 章要重點(diǎn)講的并發(fā)問題。2.4 住院表與床位表住院模塊比門診多一層「床位占用」的狀態(tài)管理。床位表記錄每個床位的占用情況住院登記表記錄患者從入院到出院的全過程。CREATE TABLE bed ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dept_id INT NOT NULL, bed_no VARCHAR(16) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0空閑 1占用, UNIQUE KEY uk_dept_bed (dept_id, bed_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE admission ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id BIGINT NOT NULL, bed_id BIGINT NOT NULL, in_time DATETIME DEFAULT CURRENT_TIMESTAMP, out_time DATETIME COMMENT 出院時間為空表示在院, status TINYINT DEFAULT 0 COMMENT 0在院 1已出院 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;bed表的status和admission表的status要同步更新入院時床位置 1出院時床位回 0。out_time為空是判斷「當(dāng)前在院」的常用方式比單獨(dú)加一個字段更省事。3. 掛號與門診模塊的 Java 接口實(shí)現(xiàn)數(shù)據(jù)庫建好只是骨架真正讓業(yè)務(wù)跑起來的是 Java 層的接口實(shí)現(xiàn)。這一章按「掛號 → 接診 → 開方」的順序把核心接口和事務(wù)邊界講清楚。3.1 掛號接口生成就診流水號與防重復(fù)掛號掛號接口要做三件事校驗(yàn)患者和醫(yī)生是否存在、生成唯一就診流水號、寫入掛號記錄。防重復(fù)掛號是重點(diǎn)同一個患者同一天同一科室不應(yīng)該掛兩次。Service public class RegistrationService { Autowired private RegistrationMapper registrationMapper; Transactional(rollbackFor Exception.class) public String register(Long patientId, Long doctorId) { // 1. 校驗(yàn)當(dāng)天是否已掛過該醫(yī)生的號 int count registrationMapper.countToday(patientId, doctorId); if (count 0) { throw new BizException(今日已掛過該醫(yī)生的號); } // 2. 生成就診流水號日期 6位隨機(jī) String visitNo new SimpleDateFormat(yyyyMMdd).format(new Date()) String.format(%06d, new Random().nextInt(1000000)); // 3. 寫入掛號記錄狀態(tài)默認(rèn) 0 待就診 Registration reg new Registration(); reg.setPatientId(patientId); reg.setDoctorId(doctorId); reg.setVisitNo(visitNo); reg.setStatus(0); registrationMapper.insert(reg); return visitNo; } }Transactional保證校驗(yàn)和寫入在同一事務(wù)里避免并發(fā)下兩個請求都通過校驗(yàn)。countToday對應(yīng)的 SQL 用DATE(reg_time) CURDATE()過濾當(dāng)天。流水號用日期加隨機(jī)數(shù)簡單夠用如果要求絕對不重復(fù)可以換成數(shù)據(jù)庫自增序列或雪花算法。3.2 門診接診接口狀態(tài)流轉(zhuǎn)與門診記錄寫入醫(yī)生接診時要把掛號狀態(tài)從 0 改成 1同時寫入門診記錄。這兩步必須原子完成。Transactional(rollbackFor Exception.class) public void seeDoctor(Long registrationId, String diagnosis, String advice) { Registration reg registrationMapper.selectById(registrationId); if (reg null || reg.getStatus() ! 0) { throw new BizException(掛號狀態(tài)異常無法接診); } // 1. 更新掛號狀態(tài)為已就診 registrationMapper.updateStatus(registrationId, 1); // 2. 寫入門診記錄 OutpatientRecord record new OutpatientRecord(); record.setRegistrationId(registrationId); record.setDiagnosis(diagnosis); record.setAdvice(advice); recordMapper.insert(record); }先查狀態(tài)再更新是為了防止對已取消或已就診的掛號重復(fù)接診。updateStatus的 SQL 里最好再加一個AND status 0條件做成樂觀鎖這樣即使并發(fā)也不會重復(fù)更新。3.3 處方開立與藥房扣庫存的事務(wù)處理處方開立是門診和藥房的交界點(diǎn)。醫(yī)生選藥、填數(shù)量系統(tǒng)寫處方明細(xì)并扣庫存整個過程必須在一個事務(wù)里。Transactional(rollbackFor Exception.class) public void createPrescription(Long recordId, ListPrescriptionItem items) { Prescription pres new Prescription(); pres.setRecordId(recordId); pres.setStatus(0); prescriptionMapper.insert(pres); for (PrescriptionItem item : items) { item.setPrescriptionId(pres.getId()); prescriptionItemMapper.insert(item); // 扣庫存帶庫存充足校驗(yàn) int affected medicineMapper.reduceStock(item.getMedicineId(), item.getNum()); if (affected 0) { throw new BizException(藥品庫存不足 item.getMedicineId()); } } }reduceStock的 SQL 是UPDATE medicine SET stock stock - #{num} WHERE id #{id} AND stock #{num}。用stock num做條件更新是防止超賣最簡單有效的方式比先查再改更安全。任何一條扣減失敗整個事務(wù)回滾處方也不會留下半截?cái)?shù)據(jù)。4. 住院管理與并發(fā)掛號的排錯實(shí)戰(zhàn)前面把主流程跑通了但真實(shí)系統(tǒng)里最容易翻車的是住院狀態(tài)同步和掛號并發(fā)。這一章講幾個實(shí)際會遇到的坑和排查方法。4.1 住院登記與床位狀態(tài)同步住院登記時如果只寫 admission 表而忘了更新 bed 表就會出現(xiàn)「床位顯示空閑但實(shí)際有人住」的情況。正確做法是把兩步放進(jìn)一個事務(wù)并且用條件更新防止重復(fù)占床。Transactional(rollbackFor Exception.class) public void admit(Long patientId, Long bedId) { // 條件更新只有空閑床位才能被占用 int affected bedMapper.occupy(bedId); if (affected 0) { throw new BizException(床位已被占用); } Admission adm new Admission(); adm.setPatientId(patientId); adm.setBedId(bedId); adm.setStatus(0); admissionMapper.insert(adm); }occupy的 SQL 是UPDATE bed SET status 1 WHERE id #{id} AND status 0。返回 0 說明床位已被別人搶走直接拋異常。出院時反向操作UPDATE bed SET status 0 WHERE id #{id}同時把 admission 的 status 置 1、out_time 填當(dāng)前時間。4.2 掛號并發(fā)下的重復(fù)流水號排查高并發(fā)掛號時如果流水號生成邏輯不夠嚴(yán)謹(jǐn)可能出現(xiàn)重復(fù)。排查思路是先看數(shù)據(jù)庫唯一索引有沒有報(bào)錯再看生成邏輯?,F(xiàn)象可能原因排查方法插入報(bào) Duplicate entry流水號重復(fù)查visit_no唯一索引看生成邏輯掛號成功但查不到事務(wù)未提交檢查Transactional是否生效同一患者掛兩次校驗(yàn)未加鎖看 countToday 是否在事務(wù)內(nèi)常見做法是把流水號生成改成數(shù)據(jù)庫自增或 Redis 原子自增避免隨機(jī)數(shù)碰撞。如果堅(jiān)持用隨機(jī)數(shù)至少把隨機(jī)位數(shù)加到 8 位以上并保留唯一索引兜底。4.3 藥房庫存扣減失敗的定位庫存扣減失敗通常有三種庫存不足、藥品不存在、并發(fā)超賣。定位時先看reduceStock返回的 affected 值。int affected medicineMapper.reduceStock(medicineId, num); if (affected 0) { // 區(qū)分是庫存不足還是藥品不存在 Medicine m medicineMapper.selectById(medicineId); if (m null) { throw new BizException(藥品不存在); } throw new BizException(庫存不足當(dāng)前庫存 m.getStock()); }這樣報(bào)錯信息更明確前端能直接提示用戶。如果日志里頻繁出現(xiàn)庫存不足但實(shí)際庫存充足就要檢查是不是有并發(fā)事務(wù)在扣減考慮給藥品行加行鎖或改用隊(duì)列削峰。提示所有涉及狀態(tài)流轉(zhuǎn)的更新都建議帶上原狀態(tài)作為 WHERE 條件做成樂觀鎖這是排查并發(fā)問題最省事的習(xí)慣。5. 用 EXPLAIN 和慢查詢?nèi)罩掘?yàn)證醫(yī)院管理系統(tǒng)的數(shù)據(jù)庫性能系統(tǒng)能跑通之后下一步是看它跑得快不快。醫(yī)院管理系統(tǒng)的查詢熱點(diǎn)集中在「按患者查就診歷史」「按醫(yī)生查當(dāng)日掛號」「按科室查空閑床位」這幾條 SQL 值得用 EXPLAIN 過一遍。5.1 用 EXPLAIN 看掛號查詢的索引命中先看一條典型查詢查某患者的所有掛號記錄。EXPLAIN SELECT r.id, r.visit_no, d.name AS doctor_name, r.status FROM registration r JOIN doctor d ON r.doctor_id d.id WHERE r.patient_id 1001 ORDER BY r.reg_time DESC;如果type是 ALL說明 registration 表走了全表掃描需要在patient_id上建索引。執(zhí)行ALTER TABLE registration ADD INDEX idx_patient (patient_id, reg_time);后type應(yīng)該變成 refkey顯示 idx_patient。聯(lián)合索引把reg_time也帶上是因?yàn)榕判蛞沧哌@個索引能省一次 filesort。5.2 慢查詢?nèi)罩径ㄎ婚T診統(tǒng)計(jì)接口門診統(tǒng)計(jì)接口通常要按日期聚合數(shù)據(jù)量大時容易慢。開啟慢查詢?nèi)罩? my.cnf 中配置 slow_query_log 1 slow_query_log_file /var/log/mysql/slow.log long_query_time 1重啟后觀察 slow.log找出執(zhí)行超過 1 秒的 SQL。常見問題是統(tǒng)計(jì)接口用了SELECT *加多層子查詢改成先按日期范圍過濾再聚合配合create_time上的索引通常能快一個數(shù)量級。5.3 住院床位查詢的覆蓋索引優(yōu)化查空閑床位是高頻操作SQL 是SELECT id, bed_no FROM bed WHERE dept_id ? AND status 0。建一個(dept_id, status, bed_no)的聯(lián)合索引讓查詢直接走覆蓋索引不用回表。ALTER TABLE bed ADD INDEX idx_dept_status (dept_id, status, bed_no);建完后用 EXPLAIN 確認(rèn)Extra列出現(xiàn)Using index說明覆蓋索引生效。這一步對住院模塊的響應(yīng)速度提升最明顯因?yàn)榇参徊樵冊谌朐旱怯涰撁鏁活l繁調(diào)用。5.4 一個容易被忽略的技巧用狀態(tài)字段做分區(qū)鍵如果掛號表數(shù)據(jù)量增長很快可以考慮按status或reg_time做分區(qū)。但更實(shí)用的技巧是把「待就診」和「已就診」的查詢分開待就診數(shù)據(jù)量小單獨(dú)建一張熱表或加緩存歷史數(shù)據(jù)留在主表。這樣既不用改表結(jié)構(gòu)又能讓高頻查詢始終命中小數(shù)據(jù)集。我一般會先加索引觀察一周確認(rèn)瓶頸確實(shí)在掛號表再考慮分區(qū)避免過度設(shè)計(jì)。本文還有配套的精品資源點(diǎn)擊獲取