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