简介:本资源是面向高校数据库课程学习者与Java初学者的Oracle数据库课程设计实践项目,聚焦医院信息管理系统的完整数据建模与应用开发。项目提供从ER图到关系模型(含配套PNG图)、SQL建表与初始化脚本、Java后端业务逻辑(36个核心类)及数据库连接配置(database.properties)的一体化实现,可直接用于课程设计、大作业或毕设参考。压缩包共45个文件,含36个Java源码(覆盖患者、医生、挂号、病历等模块)、1个SQL建表与测试语句文件、1个数据库配置文件、1份README说明文档及许可证等辅助文件,整体仅323KB,轻量易部署。已有380人下载学习,特别适合希望打通Oracle数据库设计、SQL编写与Java JDBC集成全流程的学习者,代码结构清晰、注释规范,且支持快速修改数据库连接地址以适配本地环境。
1. 项目概述与核心价值
最近在整理过去的项目资料,翻到了大学时期做的那个医院系统数据库课程设计,用的是Java和Oracle。现在回头看,这个项目虽然带着浓厚的“学生作业”气息,但它确实是一个绝佳的、能串起后端开发、数据库设计、业务逻辑理解乃至团队协作的综合实践案例。很多刚入门Java后端或者数据库的朋友,可能觉得学了一堆零散的知识点,比如JDBC、SQL、Spring Boot,但不知道怎么把它们捏合成一个能跑起来的、有实际业务意义的系统。这个医院系统的设计,恰好能填补这个空白。
简单来说,这个项目要构建的是一个简化版的医院信息管理系统(HIS)后台核心。它不是一个花哨的Web界面,而是聚焦于后端服务和数据库。你需要设计一套合理的数据库表结构来存储医院的核心数据,比如病人、医生、科室、药品、挂号记录、处方、收费明细等。然后,通过Java编写程序,提供对这些数据的增删改查(CRUD)接口,并实现一些基础的业务逻辑,比如挂号时检查医生排班、开药时扣减库存并计算费用。最终,它应该能通过控制台或者简单的API,模拟完成一次从患者挂号、医生看诊、开具处方到收费结算的完整流程。
这个项目的价值在哪里?首先,它强迫你进行真正的数据库设计。你不是在操作现成的表,而是要从零开始思考:医院有哪些实体?它们之间的关系是什么(一对一、一对多、多对多)?如何设计主键、外键来保证数据的完整性和关联性?其次,它打通了Java与数据库的链路。你会亲手用JDBC(或者后续学MyBatis、JPA)去连接Oracle,执行复杂的多表关联查询、事务处理。最后,它引入了初步的业务逻辑思维。编程不只是CRUD,还要考虑“挂号时如果该医生号已满怎么办?”、“开药时库存不足怎么办?”这样的业务规则。完成这个项目,你对一个信息系统的“数据层”和“业务逻辑层”会有非常具象的认识。
2. 整体架构设计与技术选型考量
做任何软件项目,动手敲代码之前,先想清楚架构和用什么技术,能避免后期大量的返工。对于这个课程设计级别的医院系统,我们采用经典的三层架构,但会做一些适合教学和实操的简化。
2.1 为什么是Java + Oracle?
这是一个非常经典且在企业中仍有广泛应用的组合。选择它们作为技术栈,有很强的现实意义和教学目的。
- Java的稳定性与生态:Java以其跨平台、健壮性和丰富的开源生态著称。对于医院系统这类需要高可靠性、长期维护的系统,Java是一个稳妥的选择。我们可以从最基础的JDBC开始,理解数据库操作的底层原理,然后可以平滑过渡到使用MyBatis或Spring Data JPA来提升开发效率。网络上关于Java连接Oracle的资料也极为丰富,遇到问题容易找到解决方案。
- Oracle数据库的强大与严谨:Oracle是功能最强大的商业关系型数据库之一,尤其在处理复杂事务、海量数据和高并发方面有优势。虽然对于课程设计来说有点“杀鸡用牛刀”,但学习它有助于建立严谨的数据库观念。Oracle的PL/SQL、序列、触发器、存储过程等特性,能让你深刻理解数据库不仅能存数据,还能处理复杂的业务逻辑和数据约束。使用Oracle进行设计,会让你更注重数据完整性、事务安全性和性能优化。
- 组合的实践意义:很多传统行业、金融、大型企业的核心系统仍是Java+Oracle的架构。掌握这个组合,对你理解遗留系统、应对相关面试或工作都有直接帮助。它迫使你关注连接池配置、SQL优化、事务隔离级别等在实际开发中至关重要的问题。
注意:Oracle数据库的安装和配置相对复杂,且商业使用需要许可证。对于课程设计或学习,务必使用其提供的免费版本(如Oracle Database Express Edition, XE),并严格遵守其使用条款。也可以考虑在虚拟机中安装,避免污染主机环境。
2.2 系统核心模块拆解
我们的简化版医院系统主要包含以下几个核心模块,每个模块对应一组数据库表和相关的Java业务类:
- 基础信息模块:这是系统的基石。包括
科室表、医生表、患者表、药品信息表、收费项目表。这部分设计要规范,比如医生隶属于某个科室,药品要有分类、规格、单价和库存。 - 业务流程模块:这是系统的核心动态部分。主要包括:
- 挂号管理:
挂号记录表。记录患者挂某个医生某时段的号,涉及患者ID、医生ID、挂号时间、挂号费用、状态(已挂号、已就诊、已取消)等。这里要关联医生排班信息(可以简单设计在医生表或单独排班表)。 - 诊疗与处方管理:
处方头表和处方明细表。这是典型的一对多关系。一次看诊(对应一个挂号记录)产生一张处方头(包含处方号、患者ID、医生ID、开方时间、总金额)。处方明细则记录具体开了什么药(药品ID)、数量、用法用量、单价、小计金额。 - 收费管理:
收费单表和收费明细表。同样是一对多。一次结算(可能对应一张或多张处方)生成一张收费单。收费明细记录收费项目(药品费、检查费、治疗费等)、金额。
- 挂号管理:
- 系统支持模块:包括用户权限管理(简单的用户表、角色表)、数据字典(如性别、药品类型、收费类型等)、系统日志等。对于课程设计,可以适当简化。
2.3 架构流程图与数据流
一个清晰的架构图能帮你理清思路。虽然我们不能用Mermaid,但可以用文字描述这个数据流:
- 患者就诊流程数据流:
- 患者注册/登录 -> 查询科室医生 -> 创建挂号记录(锁定号源) -> 医生接诊,查询患者历史 -> 开具处方(插入处方头及明细,同时预扣药品库存) -> 生成收费单(根据处方明细计算) -> 患者缴费(更新收费单状态,若成功则正式扣减库存;若失败则释放预扣库存) -> 患者取药。
- 后台管理数据流:
- 管理员维护科室、医生、药品基础信息 -> 医生维护个人排班 -> 财务人员日结、统计报表。
这个流程中,库存扣减和收费状态更新必须在一个数据库事务中完成,以确保数据一致性,避免“药开了但没收费”或“钱收了但没药了”的情况。这是业务逻辑的关键点。
3. 数据库详细设计与核心表结构解析
数据库设计是整个系统的灵魂。设计得好,后续编程事半功倍;设计得差,则举步维艰。这里我们遵循第三范式(3NF)进行基本设计,并在必要时为了性能考虑进行适当的反规范化。
3.1 核心实体关系模型(ER)思路
在画ER图或建表之前,先想清楚核心实体及其关系:
- 患者可以有多条挂号记录。(一对多)
- 医生可以有多条挂号记录和处方。(一对多)
- 一个挂号记录对应一次诊疗,产生一张处方头。(一对一或一对多,假设一次就诊一张处方)
- 一张处方头包含多条处方明细。(一对多)
- 一条处方明细对应一种药品。(多对一)
- 一张收费单对应一个患者的一次结算,包含多条收费明细。(一对多)
- 一条收费明细可以关联处方明细(药品费)或其他收费项目。
3.2 关键表结构定义与字段说明
以下是一些核心表的建表语句示例,包含了字段注释、数据类型和约束。使用Oracle语法。
-- 1. 科室表 CREATE TABLE department ( dept_id NUMBER(10) PRIMARY KEY, -- 科室ID,主键 dept_code VARCHAR2(20) NOT NULL UNIQUE, -- 科室编码,如 'NK'(内科) dept_name VARCHAR2(50) NOT NULL, -- 科室名称 dept_location VARCHAR2(100), -- 科室位置 dept_phone VARCHAR2(20), status CHAR(1) DEFAULT '1' -- 状态 1:启用 0:停用 ); COMMENT ON TABLE department IS '科室信息表'; COMMENT ON COLUMN department.status IS '状态 (1:启用, 0:停用)'; -- 2. 医生表 CREATE TABLE doctor ( doctor_id NUMBER(10) PRIMARY KEY, -- 医生ID doctor_no VARCHAR2(20) NOT NULL UNIQUE, -- 工号 doctor_name VARCHAR2(50) NOT NULL, dept_id NUMBER(10) NOT NULL, -- 所属科室ID,外键 title VARCHAR2(30), -- 职称 specialty VARCHAR2(200), -- 专长 schedule_info CLOB, -- 排班信息(可存储JSON文本或引用排班表) status CHAR(1) DEFAULT '1', CONSTRAINT fk_doctor_dept FOREIGN KEY (dept_id) REFERENCES department(dept_id) ); -- 3. 患者表 CREATE TABLE patient ( patient_id NUMBER(10) PRIMARY KEY, patient_no VARCHAR2(20) NOT NULL UNIQUE, -- 病历号 patient_name VARCHAR2(50) NOT NULL, gender CHAR(1), -- 'M':男, 'F':女 birth_date DATE, id_card VARCHAR2(18), -- 身份证号 phone VARCHAR2(20), address VARCHAR2(200), create_time DATE DEFAULT SYSDATE ); -- 4. 药品信息表 (关键:库存和价格管理) CREATE TABLE drug ( drug_id NUMBER(10) PRIMARY KEY, drug_code VARCHAR2(30) NOT NULL UNIQUE, -- 药品编码 drug_name VARCHAR2(100) NOT NULL, drug_type VARCHAR2(30), -- 药品类型(中药/西药/器械) specification VARCHAR2(100), -- 规格,如 '0.5g*24片' unit VARCHAR2(10), -- 单位,如 '盒','瓶' manufacturer VARCHAR2(100), -- 生产厂家 price NUMBER(10, 2) NOT NULL CHECK (price >= 0), -- 单价 stock NUMBER(10) DEFAULT 0 CHECK (stock >= 0), -- 当前库存 min_stock NUMBER(10) DEFAULT 0, -- 最低库存预警线 status CHAR(1) DEFAULT '1' ); -- 5. 挂号记录表 (核心业务表) CREATE TABLE registration ( reg_id NUMBER(10) PRIMARY KEY, reg_no VARCHAR2(30) NOT NULL UNIQUE, -- 挂号单号,可规则生成如 REG202405200001 patient_id NUMBER(10) NOT NULL, doctor_id NUMBER(10) NOT NULL, reg_time DATE DEFAULT SYSDATE, -- 挂号时间 reg_date DATE NOT NULL, -- 就诊日期 time_slot VARCHAR2(10), -- 时段,如 '上午','下午' reg_fee NUMBER(10, 2) DEFAULT 0, -- 挂号费 status VARCHAR2(20) DEFAULT '已挂号', -- 状态: 已挂号/已就诊/已取消/已退号 CONSTRAINT fk_reg_patient FOREIGN KEY (patient_id) REFERENCES patient(patient_id), CONSTRAINT fk_reg_doctor FOREIGN KEY (doctor_id) REFERENCES doctor(doctor_id) ); -- 为挂号记录表创建索引以提高查询效率 CREATE INDEX idx_reg_patient ON registration(patient_id); CREATE INDEX idx_reg_doctor_date ON registration(doctor_id, reg_date); -- 6. 处方头表 CREATE TABLE prescription ( pres_id NUMBER(10) PRIMARY KEY, pres_no VARCHAR2(30) NOT NULL UNIQUE, -- 处方号 reg_id NUMBER(10) NOT NULL, -- 关联的挂号ID patient_id NUMBER(10) NOT NULL, doctor_id NUMBER(10) NOT NULL, total_amount NUMBER(12, 2) DEFAULT 0, -- 处方总金额 create_time DATE DEFAULT SYSDATE, diagnosis CLOB, -- 诊断信息 CONSTRAINT fk_pres_reg FOREIGN KEY (reg_id) REFERENCES registration(reg_id), CONSTRAINT fk_pres_patient FOREIGN KEY (patient_id) REFERENCES patient(patient_id), CONSTRAINT fk_pres_doctor FOREIGN KEY (doctor_id) REFERENCES doctor(doctor_id) ); -- 7. 处方明细表 (业务逻辑关键,涉及库存预扣) CREATE TABLE prescription_detail ( detail_id NUMBER(10) PRIMARY KEY, pres_id NUMBER(10) NOT NULL, drug_id NUMBER(10) NOT NULL, quantity NUMBER(10, 2) NOT NULL CHECK (quantity > 0), -- 数量,支持小数(如0.5盒) dosage VARCHAR2(100), -- 用法用量 unit_price NUMBER(10, 2) NOT NULL, -- 开药时单价(快照,避免后续药品调价影响历史记录) subtotal NUMBER(12, 2) GENERATED ALWAYS AS (quantity * unit_price) VIRTUAL, -- 小计,计算列 reserved_stock NUMBER(10) DEFAULT 0, -- 预扣库存数量(用于实现库存锁定) CONSTRAINT fk_detail_pres FOREIGN KEY (pres_id) REFERENCES prescription(pres_id) ON DELETE CASCADE, CONSTRAINT fk_detail_drug FOREIGN KEY (drug_id) REFERENCES drug(drug_id) );3.3 设计要点与避坑指南
- 主键选择:我强烈建议使用无业务意义的数字序列(Sequence)作为主键,如
patient_id,reg_id。不要用身份证号、病历号等业务字段。因为业务字段可能变更,且作为主键在关联时效率不如数字。病历号(patient_no)可以作为唯一索引(UNIQUE)存在。 - 外键约束:务必为所有关联关系创建外键约束(
FOREIGN KEY)。这是保证数据引用完整性的底线。ON DELETE CASCADE(级联删除)要慎用,通常业务上我们更倾向于逻辑删除(更新状态字段为‘无效’)。 - 数据快照:注意
prescription_detail表中的unit_price字段。这里存储的是开药时的单价,而不是实时去关联drug.price。这是因为药品价格可能会变动,我们必须保证处方历史的准确性。这是一种常见的“快照”设计。 - 库存预扣机制:
reserved_stock字段是实现“避免超卖”的关键。当医生开药时,先检查drug.stock(实际库存)是否充足,如果充足,则在插入prescription_detail时,将reserved_stock设为开药数量,并同时更新drug.stock = drug.stock - 开药数量。这个更新操作必须在同一个事务中完成。如果患者最终缴费,则将reserved_stock清零;如果取消处方或缴费失败,则需要回滚,执行drug.stock = drug.stock + reserved_stock,再将reserved_stock清零。 - 索引策略:在经常用于查询条件的字段上建立索引,如
registration表的patient_id,doctor_id,reg_date。但索引不是越多越好,会影响插入和更新速度。主键和唯一约束会自动创建索引。
4. Java后端实现与关键业务逻辑编码
数据库设计好后,我们用Java来搭建桥梁。这里我们使用最经典、最能体现原理的JDBC + Servlet结构,当然你也可以用Spring Boot简化,但理解底层JDBC至关重要。
4.1 项目结构与环境搭建
创建一个标准的Maven项目,目录结构如下:
hospital-system/ ├── pom.xml ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── hospital/ │ │ │ ├── dao/ # 数据访问层 │ │ │ ├── service/ # 业务逻辑层 │ │ │ ├── servlet/ # 控制层(接收HTTP请求) │ │ │ ├── model/ # 实体类(对应数据库表) │ │ │ ├── util/ # 工具类(数据库连接、JSON解析等) │ │ │ └── MainApp.java # 控制台入口(可选) │ │ └── resources/ │ │ └── db.properties # 数据库配置文件 │ └── test/ │ └── java/ # 单元测试pom.xml 关键依赖:
<dependencies> <!-- Oracle JDBC驱动 --> <dependency> <groupId>com.oracle.database.jdbc</groupId> <artifactId>ojdbc11</artifactId> <version>21.11.0.0</version> </dependency> <!-- Servlet API --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- JSON处理(如返回API结果) --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- 日志 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.9</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.4.11</version> </dependency> </dependencies>db.properties 配置文件:
jdbc.driver=oracle.jdbc.OracleDriver jdbc.url=jdbc:oracle:thin:@localhost:1521:xe # 根据你的Oracle实例修改 jdbc.username=HOSPITAL_DB jdbc.password=your_password4.2 数据库连接与工具类封装
使用单例模式或静态工具类管理数据库连接池(这里简化使用DBCP或HikariCP,但课程设计理解原理也可用简单连接)。
// util/DatabaseUtil.java package com.hospital.util; import java.io.InputStream; import java.sql.*; import java.util.Properties; public class DatabaseUtil { private static String driver; private static String url; private static String username; private static String password; static { try { InputStream is = DatabaseUtil.class.getClassLoader().getResourceAsStream("db.properties"); Properties props = new Properties(); props.load(is); driver = props.getProperty("jdbc.driver"); url = props.getProperty("jdbc.url"); username = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); Class.forName(driver); // 加载驱动 } catch (Exception e) { throw new RuntimeException("Failed to load database configuration", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 关闭资源的通用方法,注意关闭顺序 try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt != null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }4.3 核心业务逻辑实现:以“开具处方”为例
这是系统最复杂的业务之一,涉及多表操作和事务控制。我们分步实现。
第一步:定义实体类(Model)
// model/PrescriptionDetail.java package com.hospital.model; import java.math.BigDecimal; public class PrescriptionDetail { private Integer detailId; private Integer presId; private Integer drugId; private BigDecimal quantity; // 使用BigDecimal处理金额和数量,避免精度丢失 private String dosage; private BigDecimal unitPrice; private BigDecimal subtotal; private Integer reservedStock; // 关联的药品信息(用于显示) private Drug drug; // getters and setters... }第二步:数据访问层(DAO)
// dao/PrescriptionDao.java package com.hospital.dao; import com.hospital.model.Prescription; import com.hospital.model.PrescriptionDetail; import com.hospital.util.DatabaseUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class PrescriptionDao { // 插入处方头 public int insertPrescription(Prescription pres) throws SQLException { String sql = "INSERT INTO prescription (pres_id, pres_no, reg_id, patient_id, doctor_id, total_amount, diagnosis) VALUES (pres_seq.NEXTVAL, ?, ?, ?, ?, ?, ?)"; // 使用序列生成主键,并返回生成的主键值(这里简化处理,实际可用RETURNING子句) try (Connection conn = DatabaseUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql, new String[]{"pres_id"})) { pstmt.setString(1, pres.getPresNo()); pstmt.setInt(2, pres.getRegId()); pstmt.setInt(3, pres.getPatientId()); pstmt.setInt(4, pres.getDoctorId()); pstmt.setBigDecimal(5, pres.getTotalAmount()); pstmt.setString(6, pres.getDiagnosis()); pstmt.executeUpdate(); ResultSet rs = pstmt.getGeneratedKeys(); if (rs.next()) { return rs.getInt(1); } return -1; } } // 插入处方明细并预扣库存(关键方法) public boolean insertDetailAndLockStock(PrescriptionDetail detail, Connection conn) throws SQLException { // 1. 检查药品库存是否充足 String checkStockSql = "SELECT stock FROM drug WHERE drug_id = ? FOR UPDATE"; // 使用 FOR UPDATE 锁定该行,防止并发修改 BigDecimal currentStock; try (PreparedStatement pstmt = conn.prepareStatement(checkStockSql)) { pstmt.setInt(1, detail.getDrugId()); ResultSet rs = pstmt.executeQuery(); if (!rs.next()) { throw new SQLException("Drug not found with id: " + detail.getDrugId()); } currentStock = rs.getBigDecimal("stock"); } if (currentStock.compareTo(detail.getQuantity()) < 0) { throw new SQLException("Insufficient stock for drug id: " + detail.getDrugId() + ". Required: " + detail.getQuantity() + ", Available: " + currentStock); } // 2. 扣减库存 String updateStockSql = "UPDATE drug SET stock = stock - ? WHERE drug_id = ?"; try (PreparedStatement pstmt = conn.prepareStatement(updateStockSql)) { pstmt.setBigDecimal(1, detail.getQuantity()); pstmt.setInt(2, detail.getDrugId()); int rows = pstmt.executeUpdate(); if (rows != 1) { throw new SQLException("Failed to update stock for drug id: " + detail.getDrugId()); } } // 3. 插入处方明细,并记录预扣数量 String insertDetailSql = "INSERT INTO prescription_detail (detail_id, pres_id, drug_id, quantity, dosage, unit_price, reserved_stock) VALUES (detail_seq.NEXTVAL, ?, ?, ?, ?, ?, ?)"; try (PreparedStatement pstmt = conn.prepareStatement(insertDetailSql)) { pstmt.setInt(1, detail.getPresId()); pstmt.setInt(2, detail.getDrugId()); pstmt.setBigDecimal(3, detail.getQuantity()); pstmt.setString(4, detail.getDosage()); pstmt.setBigDecimal(5, detail.getUnitPrice()); pstmt.setBigDecimal(6, detail.getQuantity()); // 预扣库存等于开药数量 return pstmt.executeUpdate() == 1; } } }第三步:业务逻辑层(Service)这是核心,负责协调多个DAO操作,并管理事务。
// service/PrescriptionService.java package com.hospital.service; import com.hospital.dao.PrescriptionDao; import com.hospital.dao.DrugDao; import com.hospital.model.Prescription; import com.hospital.model.PrescriptionDetail; import com.hospital.util.DatabaseUtil; import java.sql.Connection; import java.sql.SQLException; import java.util.List; public class PrescriptionService { private PrescriptionDao prescriptionDao = new PrescriptionDao(); private DrugDao drugDao = new DrugDao(); /** * 开具处方(核心业务方法,包含事务) * @param prescription 处方头信息 * @param detailList 处方明细列表 * @return 处方ID,失败返回-1 */ public int createPrescription(Prescription prescription, List<PrescriptionDetail> detailList) { Connection conn = null; try { // 1. 获取连接,并关闭自动提交(开启事务) conn = DatabaseUtil.getConnection(); conn.setAutoCommit(false); // 关键步骤! // 2. 插入处方头 int presId = prescriptionDao.insertPrescription(prescription); if (presId <= 0) { conn.rollback(); return -1; } // 3. 循环插入处方明细并预扣库存 for (PrescriptionDetail detail : detailList) { detail.setPresId(presId); // 设置关联的处方ID // 获取开药时的药品单价(快照) BigDecimal currentPrice = drugDao.getDrugPriceById(detail.getDrugId(), conn); detail.setUnitPrice(currentPrice); // 插入明细并锁库存 if (!prescriptionDao.insertDetailAndLockStock(detail, conn)) { conn.rollback(); // 任何一行失败,整个事务回滚 return -1; } } // 4. 计算并更新处方总金额(可以在数据库用触发器,也可以在Java计算) BigDecimal totalAmount = detailList.stream() .map(d -> d.getUnitPrice().multiply(d.getQuantity())) .reduce(BigDecimal.ZERO, BigDecimal::add); prescriptionDao.updatePrescriptionTotalAmount(presId, totalAmount, conn); // 5. 提交事务 conn.commit(); return presId; } catch (SQLException e) { e.printStackTrace(); // 6. 发生异常,回滚事务 if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } return -1; } finally { // 7. 恢复自动提交并关闭连接(实际应用应放回连接池) if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }4.4 事务管理与并发控制要点
在上面的代码中,有几个关键点需要深刻理解:
conn.setAutoCommit(false):这是手动管理事务的开始。在此之后执行的所有SQL语句,都会在一个事务中,直到调用commit()或rollback()。FOR UPDATE子句:在checkStockSql中,我们使用了SELECT ... FOR UPDATE。这会在查询时对符合条件的行加上排他锁,直到当前事务结束。这防止了其他事务同时读取并修改同一条库存记录,从而避免了“超卖”问题。这是处理高并发库存扣减的经典手段。- 原子性操作:检查库存、扣减库存、插入明细这三个步骤被包裹在同一个事务里。要么全部成功,要么全部失败回滚。保证了数据的一致性。
- 异常处理与回滚:在
catch块中必须进行回滚操作,否则可能留下部分更新的数据,导致数据不一致。 - 连接关闭:在
finally块中确保连接被关闭或归还给连接池,并恢复自动提交模式,避免影响后续操作。
5. 功能扩展、优化与常见问题排查
完成核心流程后,一个课程设计还可以从以下几个方向进行深化和优化,这能极大提升项目的含金量。
5.1 关键功能扩展点
- 用户登录与权限控制:实现一个简单的基于角色的访问控制(RBAC)。创建
user、role、user_role表。医生只能看到自己的患者和处方,药房人员只能看到待发药的处方,管理员拥有所有权限。在Servlet中进行会话管理和接口权限拦截。 - 排班管理:设计一个
doctor_schedule表,记录医生在未来一段时间内(如一周)的排班情况(日期、时段、可挂号总数、已挂号数)。在患者挂号时,需要检查该医生该时段是否还有余号,并更新已挂号数。这同样需要事务和行锁来防止号源超发。 - 收费与发药流程:
- 收费:创建收费单,关联处方,更新收费状态。收费成功后,触发
prescription_detail的reserved_stock清零(表示预扣正式生效)。 - 发药:药房人员根据已收费的处方明细进行发药操作。可以增加一个
dispensing(发药)表,记录发药人、发药时间。发药后,可更新药品的实际出库记录。
- 收费:创建收费单,关联处方,更新收费状态。收费成功后,触发
- 数据统计与报表:编写复杂的SQL查询,为管理员提供报表。
- 每日/每月挂号量、收费金额统计。
- 医生工作量统计(接诊人数、开药金额)。
- 药品销量排行榜、库存预警报表(
stock < min_stock)。
5.2 性能与优化建议
- 使用连接池:上述示例中每次操作都新建连接,实际项目必须使用连接池(如HikariCP),极大提升性能。
- SQL优化:
- 为高频查询条件建立复合索引,如
(doctor_id, reg_date)。 - 避免在
WHERE子句中对字段进行函数操作(如WHERE TO_CHAR(reg_time, 'YYYY-MM-DD') = ?),这会使得索引失效。 - 对于大数据量分页查询,不要使用
SELECT * FROM table LIMIT 100000, 20,而应使用基于主键的范围查询。
- 为高频查询条件建立复合索引,如
- 引入框架:将原生JDBC升级为MyBatis或Spring Data JPA,能简化大量样板代码,让你更专注于业务逻辑。
- 前后端分离:将Servlet替换为Spring Boot + RESTful API,前端使用Vue/React,打造一个更现代化的项目,这对你的技能栈是很好的补充。
5.3 常见问题与排查实录
在开发过程中,你几乎一定会遇到以下问题:
- Oracle ORA-12514/TNS错误:
- 现象:
Listener refused the connection with the following error: ORA-12514... - 排查:检查
jdbc.url是否正确,格式为jdbc:oracle:thin:@//host:port/service_name或@host:port:SID。确认Oracle监听器(Listener)和数据库实例是否已启动。使用tnsping命令测试网络连通性。
- 现象:
- 中文乱码:
- 现象:插入或查询的中文显示为问号。
- 解决:确保三处编码统一为UTF-8。
- Oracle数据库字符集:
SELECT * FROM nls_database_parameters WHERE parameter LIKE '%CHARACTERSET'; - JDBC连接URL:在URL后添加参数
?useUnicode=true&characterEncoding=UTF-8(对于Oracle ojdbc8+,通常不需要)。 - Java项目文件编码(IDE设置)和编译编码。
- Oracle数据库字符集:
- 事务未生效或锁超时:
- 现象:A用户正在开药(事务未提交),B用户查询该药品库存一直等待或超时。
- 排查:确认是否正确使用了
setAutoCommit(false)和commit()/rollback()。检查SQL语句是否使用了FOR UPDATE导致行锁。在开发环境,避免长时间不提交的事务。Oracle默认的锁等待时间可能较长,可以通过SELECT * FROM v$lock查看锁信息。
- 序列(Sequence)冲突:
- 现象:主键重复插入错误。
- 排查:在DAO中获取生成的序列主键时,确保方法正确。Oracle中,在
PreparedStatement中指定需要返回的列名数组。在多线程环境下,序列本身是线程安全的,但获取和插入的逻辑需要保证在同一个连接/事务上下文中。
BigDecimal的精度问题:- 现象:金额计算出现奇怪的舍入误差。
- 解决:金额、单价等字段在Java中一律使用
BigDecimal,并且进行运算时(add,subtract,multiply)要指定精度和舍入模式,例如amount.setScale(2, RoundingMode.HALF_UP)。绝对不要使用double或float。
- Web服务器部署问题:
- 现象:将项目打包成WAR放到Tomcat下运行,报
ClassNotFoundException: oracle.jdbc.OracleDriver。 - 解决:Oracle的JDBC驱动jar包不能通过Maven依赖直接打包进WEB-INF/lib,因为它的许可证问题。需要手动将
ojdbc11.jar复制到Tomcat的lib目录下,或者将其安装到本地Maven仓库并使用<scope>provided</scope>。
- 现象:将项目打包成WAR放到Tomcat下运行,报
这个基于Java+Oracle的医院系统数据库课程设计,就像一台精密的仪器,数据库是它的骨架和存储仓,Java程序是它的控制中枢和传动系统。从ER设计到表结构创建,从JDBC连接到复杂事务处理,每一步都踩在软件开发的实地上。过程中遇到的每一个错误,从ORA编号到空指针异常,都是最好的老师。当你最终看到挂号、开药、收费的流程在你自己构建的系统里顺利跑通时,那种对整体架构和细节把控的理解,是任何理论课都无法给予的。我建议你在实现基本功能后,一定要尝试至少一个扩展功能,比如实现一个简单的权限控制,或者用更复杂的SQL做一次数据统计分析,这会让你的项目从“作业”升级为“作品”。最后,别忘了好好写你的项目文档和设计报告,把为什么这么设计、遇到了什么问题、怎么解决的思路清晰地记录下来,这本身也是一项极其重要的能力。
本文还有配套的精品资源,点击获取