ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaWeb医院药品管理系统源码解析:Servlet+JDBC实现药品库存与权限控制

JavaWeb医院药品管理系统源码解析:Servlet+JDBC实现药品库存与权限控制 简介这份资源是面向计算机相关专业学生的JavaWeb实训项目——基于JavaWeb的医院药品管理系统适合正在做课程设计、期末大作业或毕业设计的学习者也可作为Java项目实战练习的参考。项目采用B/S结构以Java为开发语言结合JSP技术与MySQL后台数据库涵盖药品信息管理、用户管理等核心模块源码与数据库脚本均已通过调试可直接运行。压缩包共422个文件约10.21MB包含96个html页面、54个js脚本、22个java源文件、44个class编译文件、14个jar依赖包以及css、xml、jsp、properties等配置与资源文件另附1个sql数据库脚本目录结构完整便于按模块查阅与二次开发。目前已有498人学习下载。对于需要快速搭建可运行项目、理解JavaWeb分层设计与数据库交互的读者这份资源能提供完整的代码框架与建库脚本帮助节省环境搭建与调试时间也方便在此基础上扩展功能或撰写设计文档。1. 从一份 JavaWeb 医院药品管理系统源码说起它能解决什么如果你正在做 JavaWeb 课程设计或者实训项目大概率会遇到一个尴尬局面需求文档写完了数据库表也画了但真正要交一个能跑起来、有完整增删改查、带权限控制的系统时时间根本不够。这份「基于 JavaWeb 的医院药品管理系统」源码包就是冲着这个场景来的。它把药品信息管理、库存出入库、供应商维护、用户角色权限这几块核心业务串成了一条完整链路配套的 SQL 数据库脚本直接导入就能用省掉了从零建表、写 DAO 层、调 JSP 页面的重复劳动。我拿到这个包之后第一件事不是急着跑而是先看目录结构和数据库脚本判断它到底是「能交作业的完整项目」还是「只有几个页面的半成品」。结论是它属于前者功能模块划分清晰后端用的是 Servlet JDBC 的经典组合前端是 JSP 原生 JavaScript没有引入 Spring 全家桶对新手来说反而更好读、更好改。适合的人群很明确——正在上 JavaWeb 课程、需要交实训大作业、或者想拿一个真实业务场景练手 Servlet 和 JSP 的人。如果你已经习惯了 SpringBoot 那一套这个项目会让你重新理解「没有框架的时候一个请求是怎么从浏览器走到数据库再走回来的」。2. 拆开压缩包先看什么目录结构、技术栈与数据库脚本2.1 目录结构里藏着项目的真实完成度解压之后不要急着往 IDE 里导先花五分钟把目录扫一遍。一个 JavaWeb 项目的完成度从目录结构就能看出七八成。常见的结构是这样的HospitalDrugSystem/ ├── src/ │ └── com/ │ └── hospital/ │ ├── dao/ # 数据访问层直接写 JDBC │ ├── entity/ # 实体类和数据库表一一对应 │ ├── servlet/ # 控制层处理请求和跳转 │ ├── util/ # 数据库连接工具类 │ └── filter/ # 编码过滤、登录拦截 ├── WebContent/ 或 web/ │ ├── css/ │ ├── js/ │ ├── jsp/ # 页面文件 │ └── WEB-INF/ │ ├── web.xml # Servlet 和 Filter 配置 │ └── lib/ # 依赖 jar 包 └── drug_system.sql # 数据库脚本看到dao、entity、servlet三层分好了说明作者是按 MVC 思路写的不是把所有逻辑塞在一个 JSP 里。filter目录存在意味着有登录拦截或者编码统一处理这两块是课程设计里最容易丢分的地方。lib目录下的 jar 包决定了你能不能直接跑——常见的是mysql-connector-java、jstl、standard这几个。如果lib是空的说明依赖需要你自己补后面我会说怎么补。2.2 技术栈判断为什么是 Servlet JDBC 而不是 SpringBoot这个项目的技术栈从目录就能反推出来没有application.properties、没有pom.xml、没有Controller注解说明它不是 SpringBoot 项目。后端是原生 Servlet 处理请求DAO 层用 JDBC 直连数据库前端 JSP 里嵌 Java 代码或者用 JSTL 标签。这套组合在今天看起来「老」但对 JavaWeb 实训来说恰恰是最合适的。原因有三点。第一课程设计考察的是你对 Servlet 生命周期、请求转发与重定向、Session 管理的理解用 SpringBoot 反而把这些细节屏蔽了答辩的时候老师问「一个请求怎么走到你的业务方法」你答不上来。第二原生 JDBC 虽然写起来啰嗦但能让你真正理解Connection、PreparedStatement、ResultSet的关系以及为什么必须关连接。第三没有 Maven 依赖管理意味着你要手动把 jar 包放进WEB-INF/lib这个过程本身就是 JavaWeb 部署的基本功。提示如果你打算把这个项目改造成 Maven 项目可以保留src下的包结构不动只加一个pom.xml引入javax.servlet-api、mysql-connector-java、jstl三个依赖然后把WebContent改名为webapp放到src/main下。改完之后用 Tomcat 插件跑比手动配 Tomcat 省事。2.3 数据库脚本怎么读先看表关系再看字段drug_system.sql这个文件是整个项目的根基导入之前先打开看一眼。常见的表有这么几张user用户表存账号密码和角色、drug药品表存药品名称、编号、价格、库存、supplier供应商表、stock_record出入库记录表。表之间的关系是药品属于某个供应商出入库记录关联药品和操作人。读脚本的时候重点关注三个地方。一是建表语句里的外键约束如果stock_record表有drug_id外键指向drug表说明删除药品时要先处理关联记录否则会报约束错误。二是初始数据脚本末尾通常有几条INSERT语句插入默认管理员账号常见的是admin/123456或者root/root这个在登录页要用。三是字符集建表语句末尾如果是DEFAULT CHARSETutf8导入时数据库也要用 utf8否则中文药品名会变乱码。-- 典型的药品表建表语句注意字段类型和约束 CREATE TABLE drug ( id int(11) NOT NULL AUTO_INCREMENT, drug_name varchar(100) NOT NULL COMMENT 药品名称, drug_code varchar(50) NOT NULL COMMENT 药品编号, price decimal(10,2) DEFAULT 0.00 COMMENT 单价, stock int(11) DEFAULT 0 COMMENT 库存数量, supplier_id int(11) DEFAULT NULL COMMENT 供应商ID, PRIMARY KEY (id), UNIQUE KEY drug_code (drug_code), KEY supplier_id (supplier_id), CONSTRAINT drug_ibfk_1 FOREIGN KEY (supplier_id) REFERENCES supplier (id) ) ENGINEInnoDB DEFAULT CHARSETutf8;这段 SQL 里drug_code加了唯一索引意味着药品编号不能重复插入重复编号会报Duplicate entry错误。supplier_id是外键删除供应商之前必须先处理该供应商下的药品否则报Cannot delete or update a parent row。这些约束在测试的时候会频繁触发提前知道能省不少排查时间。3. 把项目跑起来IDEA 配置、Tomcat 部署与数据库连接3.1 IDEA 导入项目与 Tomcat 配置的完整步骤这个项目不是 Maven 结构所以不能用「Open as Maven Project」的方式导入。正确做法是打开 IDEA选择File - New - Project from Existing Sources指向解压后的项目根目录然后选Create project from existing sources一路 Next。导入之后检查Project Structure里的Modules确认src被标记为 Sources RootWebContent被标记为 Web Resource Directory。接下来配 Tomcat。在Run - Edit Configurations里点选Tomcat Server - Local在Server标签页指定本地 Tomcat 安装目录。关键是Deployment标签页点选Artifact选项目名:war exploded。如果列表里没有这个 Artifact说明Project Structure - Artifacts里还没创建手动加一个 Web Application Exploded把WebContent目录加进去输出目录设成out/artifacts/项目名_war_exploded。# 检查 Tomcat 是否正常启动访问默认页 # 启动后在浏览器输入 http://localhost:8080/ # 项目部署后的访问地址通常是 http://localhost:8080/HospitalDrugSystem/ # 或者带上下文路径 http://localhost:8080/drug/Application context这一栏决定了访问路径。如果设成/drug那登录页就是http://localhost:8080/drug/login.jsp。设成/的话直接http://localhost:8080/login.jsp就能访问。建议设一个具体的名字避免和 Tomcat 默认应用冲突。3.2 数据库连接工具类改对四个参数就能连上项目里连接数据库的代码通常在util包下类名可能是DBUtil或者BaseDao。打开之后你会看到类似这样的代码public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/drug_system?useUnicodetruecharacterEncodingutf8useSSLfalse; private static final String USER root; private static final String PASSWORD 123456; private static final String DRIVER com.mysql.jdbc.Driver; public static Connection getConnection() throws Exception { Class.forName(DRIVER); return DriverManager.getConnection(URL, USER, PASSWORD); } }需要改的只有四个地方。URL里的drug_system换成你实际导入的数据库名USER和PASSWORD换成你本地 MySQL 的账号密码DRIVER这一行要注意MySQL 5.x 用com.mysql.jdbc.DriverMySQL 8.x 用com.mysql.cj.jdbc.Driver而且 URL 后面要加serverTimezoneAsia/Shanghai否则会报时区错误。注意如果启动时报ClassNotFoundException: com.mysql.jdbc.Driver说明WEB-INF/lib下没有 MySQL 驱动 jar 包。去 MySQL 官网或者 Maven 仓库下载对应版本的mysql-connector-java放进lib目录然后在 IDEA 的Project Structure - Libraries里把这个 jar 加进去。3.3 导入 SQL 脚本与验证数据数据库连接改好之后用 Navicat 或者命令行导入drug_system.sql。命令行方式如下# 先创建数据库如果脚本里没有 CREATE DATABASE 语句 mysql -u root -p -e CREATE DATABASE drug_system DEFAULT CHARSET utf8; # 导入脚本 mysql -u root -p drug_system drug_system.sql # 验证表是否创建成功 mysql -u root -p -e USE drug_system; SHOW TABLES;导入完成后执行SELECT * FROM user;看一眼默认账号。如果user表是空的说明脚本里没有初始数据你需要手动插一条INSERT INTO user (username, password, role) VALUES (admin, 123456, admin);role字段决定了登录后的权限常见值是admin和user。用 admin 登录能看到所有功能用 user 登录可能只能查药品不能删。这个逻辑在filter或者servlet里判断测试的时候两种角色都登一下确认权限控制生效。4. 核心功能怎么改药品增删改查与库存出入库逻辑4.1 药品列表分页查询的 SQL 与 Servlet 参数传递药品列表页是这个系统里最核心的页面通常带分页和模糊搜索。Servlet 里接收两个参数page当前页码和keyword搜索关键词。DAO 层拼 SQL 的时候要注意分页用LIMIT搜索用LIKE。// Servlet 中获取分页参数 String pageStr request.getParameter(page); int page (pageStr null || pageStr.isEmpty()) ? 1 : Integer.parseInt(pageStr); String keyword request.getParameter(keyword); if (keyword null) keyword ; // 调用 DAO 查询 ListDrug drugList drugDao.findByPage(page, 10, keyword); int totalCount drugDao.countByKeyword(keyword); int totalPage (int) Math.ceil(totalCount / 10.0); request.setAttribute(drugList, drugList); request.setAttribute(totalPage, totalPage); request.setAttribute(currentPage, page); request.getRequestDispatcher(/jsp/drug_list.jsp).forward(request, response);DAO 层的 SQL 这样写public ListDrug findByPage(int page, int pageSize, String keyword) throws Exception { String sql SELECT d.*, s.supplier_name FROM drug d LEFT JOIN supplier s ON d.supplier_id s.id WHERE d.drug_name LIKE ? OR d.drug_code LIKE ? LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setString(2, % keyword %); ps.setInt(3, (page - 1) * pageSize); ps.setInt(4, pageSize); ResultSet rs ps.executeQuery(); ListDrug list new ArrayList(); while (rs.next()) { Drug d new Drug(); d.setId(rs.getInt(id)); d.setDrugName(rs.getString(drug_name)); d.setDrugCode(rs.getString(drug_code)); d.setPrice(rs.getBigDecimal(price)); d.setStock(rs.getInt(stock)); d.setSupplierName(rs.getString(supplier_name)); list.add(d); } return list; } }LIMIT ?, ?第一个问号是偏移量第二个是每页条数。偏移量算错会导致第一页显示第二页的数据这是分页最常见的 bug。LEFT JOIN是为了在药品列表里直接显示供应商名称如果用INNER JOIN没有供应商的药品就不会出现在列表里。4.2 库存出入库事务处理与库存回滚出入库是这个系统里唯一涉及多表操作的功能。入库要同时做两件事往stock_record表插一条记录更新drug表的stock字段。这两步必须在一个事务里否则插了记录但库存没加数据就不一致了。public boolean stockIn(int drugId, int quantity, int userId) throws Exception { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 第一步插入出入库记录 String sql1 INSERT INTO stock_record (drug_id, quantity, type, user_id, create_time) VALUES (?, ?, in, ?, NOW()); try (PreparedStatement ps1 conn.prepareStatement(sql1)) { ps1.setInt(1, drugId); ps1.setInt(2, quantity); ps1.setInt(3, userId); ps1.executeUpdate(); } // 第二步更新药品库存 String sql2 UPDATE drug SET stock stock ? WHERE id ?; try (PreparedStatement ps2 conn.prepareStatement(sql2)) { ps2.setInt(1, quantity); ps2.setInt(2, drugId); ps2.executeUpdate(); } conn.commit(); // 两步都成功才提交 return true; } catch (Exception e) { if (conn ! null) conn.rollback(); // 任何一步失败就回滚 throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }setAutoCommit(false)是事务的起点commit()是终点rollback()是后悔药。注意finally块里要把autoCommit恢复成true再关连接否则连接回到池子里还是事务状态下一个请求会出问题。出库逻辑类似只是type改成out库存改成stock - ?而且出库前要先判断库存够不够// 出库前检查库存 String checkSql SELECT stock FROM drug WHERE id ? FOR UPDATE; // FOR UPDATE 加行锁防止并发出库导致库存变负FOR UPDATE这一句在高并发场景下有用课程设计里测试不出来但答辩的时候能说出来是加分项。4.3 用户角色权限拦截Filter 的写法与放行规则权限控制靠filter包下的拦截器实现。核心逻辑是判断 Session 里有没有登录用户没有就跳登录页有的话再判断角色普通用户访问管理员功能时拦截。public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 放行登录页、登录接口、静态资源 if (uri.contains(login.jsp) || uri.contains(LoginServlet) || uri.contains(.css) || uri.contains(.js)) { chain.doFilter(req, resp); return; } // 检查登录状态 Object user request.getSession().getAttribute(currentUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }web.xml里配置这个 Filter 拦截所有请求filter filter-nameAuthFilter/filter-name filter-classcom.hospital.filter.AuthFilter/filter-class /filter filter-mapping filter-nameAuthFilter/filter-name url-pattern/*/url-pattern /filter-mappingurl-pattern写/*表示拦截所有请求包括 JSP 和 Servlet。放行规则写在 Filter 内部比在web.xml里配多个url-pattern更灵活。注意登录接口的路径要和放行条件里的字符串匹配如果登录 Servlet 的映射是/login放行条件里就要写uri.contains(/login)写错了会导致登录请求也被拦截陷入死循环。5. 避坑与排查从 404 到乱码的五个血泪经验5.1 启动后 404上下文路径和 web.xml 版本问题现象Tomcat 启动没报错但访问http://localhost:8080/项目名/显示 404。原因通常有两个。一是Application context设错了比如设成了/HospitalDrugSystem但实际访问时没加这个前缀。二是web.xml的web-app标签版本和 Tomcat 版本不匹配比如 Tomcat 9 用了web-app_4_0.xsd但 Servlet 版本写的是 2.5导致容器不识别。解决先在 IDEA 的 Tomcat 配置里确认Application context的值然后检查web.xml头部声明。Tomcat 9 对应 Servlet 4.0头部应该长这样web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0如果项目里用的是 2.5 版本Tomcat 9 也能跑但 JSTL 标签可能不生效。最省事的做法是把版本改成和 Tomcat 匹配的。5.2 中文乱码请求编码、响应编码和数据库编码三处都要改现象表单提交的药品名称在数据库里显示成????或者页面显示成乱码。原因POST 请求默认用 ISO-8859-1 解码JSP 页面默认 contentType 也可能是 ISO-8859-1数据库连接 URL 没指定字符集。解决分三步。第一步在 Filter 里统一设置请求和响应编码request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8);第二步每个 JSP 页面头部加% page contentTypetext/html;charsetUTF-8 languagejava %第三步数据库连接 URL 加useUnicodetruecharacterEncodingutf8建表语句用DEFAULT CHARSETutf8。三处都改了还乱码的话检查 MySQL 的my.ini里character-set-server是不是 utf8。5.3 数据库连接报时区错误MySQL 8.x 的 serverTimezone 参数现象java.sql.SQLException: The server time zone value ?D1ú±ê×?ê±?? is unrecognized。原因MySQL 8.x 驱动要求 URL 里指定时区不指定就报这个错。解决在 URL 后面加serverTimezoneAsia/Shanghai。完整 URL 变成jdbc:mysql://localhost:3306/drug_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai同时确认驱动类名是com.mysql.cj.jdbc.Driver而不是com.mysql.jdbc.Driver。用错驱动类名会报ClassNotFoundException或者连接不上。5.4 JSTL 标签不解析jar 包缺失或版本冲突现象JSP 页面里写的c:forEach标签原样输出到浏览器没有被解析成循环。原因WEB-INF/lib下缺少jstl.jar和standard.jar或者只放了一个。解决把jstl-1.2.jar和standard-1.1.2.jar都放进lib目录。如果用的是 Tomcat 10JSTL 的包名从javax.servlet.jsp.jstl变成了jakarta.servlet.jsp.jstl需要换用jakarta.servlet.jsp.jstl-api和对应实现。版本不匹配的话标签库 URI 也要改http://java.sun.com/jsp/jstl/core在 Jakarta 版本里变成了jakarta.tags.core。5.5 修改代码后页面没变化Tomcat 缓存与重新部署现象改了 JSP 或者 Java 代码重启 Tomcat 后页面还是旧的。原因IDEA 的 Artifact 没有重新构建或者 Tomcat 的work目录缓存了编译后的 JSP。解决在 Tomcat 配置的Server标签页里把On Update action设成Update classes and resourcesOn frame deactivation也设成一样。然后点Build - Rebuild Project再重启 Tomcat。如果还不行手动删掉 Tomcat 安装目录下的work/Catalina/localhost/项目名文件夹强制重新编译 JSP。6. 进阶改造把 JDBC 换成连接池与分页查询优化6.1 用 Druid 连接池替换裸 JDBC原生 JDBC 每次请求都新建连接、关闭连接开销大。课程设计里数据量小感觉不出来但如果你想把这个项目写进简历面试官问「连接池用过吗」的时候得答得上来。改造方式是把DBUtil里的DriverManager.getConnection换成 Druid 的DruidDataSource。public class DBUtil { private static DruidDataSource dataSource; static { dataSource new DruidDataSource(); dataSource.setUrl(jdbc:mysql://localhost:3306/drug_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai); dataSource.setUsername(root); dataSource.setPassword(123456); dataSource.setDriverClassName(com.mysql.cj.jdbc.Driver); dataSource.setInitialSize(5); // 初始连接数 dataSource.setMaxActive(20); // 最大连接数 dataSource.setMinIdle(5); // 最小空闲连接 dataSource.setMaxWait(3000); // 获取连接超时时间毫秒 } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }initialSize是启动时创建的连接数maxActive是并发高峰时最多能拿到的连接数maxWait是拿不到连接时等多久报错。这三个参数要根据实际并发量调课程设计里设成 5、20、3000 就够用。改造完之后 DAO 层的代码不用动因为getConnection()的返回类型还是Connection。6.2 分页查询的 count 优化与索引建议原来的分页实现里每次翻页都要执行一次SELECT COUNT(*)算总数。数据量小的时候没问题药品表到几万条的时候 count 会变慢。优化方式有两个一是给drug_name和drug_code加索引让LIKE查询走索引二是把 count 结果缓存起来只在搜索条件变化时重新查。-- 给搜索字段加索引 ALTER TABLE drug ADD INDEX idx_drug_name (drug_name); ALTER TABLE drug ADD INDEX idx_drug_code (drug_code); -- 如果搜索用的是前缀匹配索引能生效 SELECT * FROM drug WHERE drug_name LIKE 阿莫% LIMIT 0, 10; -- 如果用的是 %关键词%索引不生效走全表扫描 SELECT * FROM drug WHERE drug_name LIKE %阿莫% LIMIT 0, 10;LIKE 阿莫%能走索引LIKE %阿莫%不能。如果业务上允许前缀搜索把 SQL 改成前缀匹配能明显提速。不允许的话考虑用全文索引或者把搜索功能交给 Elasticsearch但那就超出这个项目的范围了。6.3 一个我每次改完 DAO 都会做的验证习惯改完 DAO 层的 SQL 之后我不会直接启动 Tomcat 点页面而是先写一个main方法单独测 DAO。这样做的好处是如果 SQL 写错了异常堆栈直接指向具体行号不用在 Servlet 和 JSP 之间来回排查。public static void main(String[] args) throws Exception { DrugDao dao new DrugDao(); // 测试分页查询 ListDrug list dao.findByPage(1, 5, ); System.out.println(第一页条数 list.size()); // 测试库存更新 boolean result dao.stockIn(1, 10, 1); System.out.println(入库结果 result); }这个习惯是从一次翻车经历里养成的当时改了一个UPDATE语句少写了一个WHERE条件结果把整张表的库存都改成了同一个值。如果先跑main方法一眼就能看出影响行数不对。从那以后我每次改完 DAO 都强制走一遍单元测试确认影响行数符合预期再往页面上接。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取
返回列表