药店药品进销存管理Java Web系统(JSP+MySQL+MyEclipse,含源码与数据库脚本)
简介:一套可直接运行的药店药品进销存管理系统,基于Java Web技术栈开发,前端用JSP实现页面交互,后端逻辑由Java类处理,数据存储采用MySQL数据库,开发环境为MyEclipse,支持Tomcat 7.0部署。系统提供完整的药品生命周期管理功能:药品信息的添加、修改、删除和查询;药品分类维护;多角色用户体系(管理员、操作员等)及对应权限控制;药房基础资料配置;药品入库、出库登记与记录追溯;公告信息发布与管理。所有页面均配套JSP文件,如登录页login.jsp、主界面main.jsp、出入库操作页(ruku/、chuku/目录下)、药品管理yaopin/、用户管理yonghu/等;后台Java代码按包结构组织在src/com/路径下;数据库脚本jspyyygl.sql已准备就绪,导入即可使用;默认管理员账号admin/admin,数据库连接参数在DBO.java中配置。项目目录结构规范,包含WebRoot、WEB-INF、CSS样式、图片资源、业务JSP页面及配置文件,适合教学实践、课程设计或小型药房信息化快速落地。
1. 项目概述:为什么一个“老派”技术栈反而更适合药房信息化落地?
你可能刚看到标题里的 JSP、MyEclipse、Tomcat 7.0 就下意识皱眉——这不都是十年前的技术吗?现在都流行 Spring Boot + Vue 了,怎么还拿 JSP 做毕业设计?别急,先放下技术成见,咱们从药房的真实场景聊起。
我做过三年社区药房的IT支持,也帮五家乡镇卫生院做过药房系统迁移。最深的体会是:药房不是互联网公司,它要的是“稳、准、快、省”,而不是“高并发、微服务、云原生”。 一家日均处方量80~200张的社区药房,管理员可能是45岁的药师,电脑操作熟练度停留在“会用微信、能打字、知道怎么点保存按钮”的水平;药房老板最关心的不是系统用了什么架构模式,而是“今天新进的20盒阿莫西林,能不能三分钟内录进系统、马上查得到库存、月底报表能直接打印交财务”。
这套 药店药品进销存管理Java Web系统,恰恰是为这类真实场景量身定制的。它用 JSP + Servlet + JDBC 的经典三层结构(表现层-JSP、控制层-logincheck.jsp等跳转逻辑、数据访问层-DBO.java封装),没有抽象过度的框架层,没有需要反复调试的前后端跨域问题,没有npm install失败导致整个项目跑不起来的尴尬。你下载下来,解压,导入MyEclipse,改两行数据库配置,启动Tomcat——5分钟内就能在浏览器里输入 admin/admin 登录进去,开始录入第一盒板蓝根。这种“开箱即用”的确定性,在教学和小型药房场景里,比任何炫技都珍贵。
关键词里提到的 “药店进销存”“JSP药品系统”“MySQL药房管理”,不是技术名词堆砌,而是三个锚点:
- “药店进销存”定义了业务边界——它不处理医保结算、不对接HIS系统、不搞AI用药推荐,只聚焦“药在哪、进了多少、卖了多少、还剩几盒、谁经手的”这五个核心问题;
- “JSP药品系统”说明了技术选型逻辑——JSP天然适合表单密集、CRUD为主、交互不复杂的后台管理系统,每个页面就是一个独立功能单元(比如 chuku/add.jsp 就只干“新增出库单”这一件事),学生看懂一个页面就能上手改另一个,老师批改作业时一眼就能定位到业务逻辑在哪一行;
- “MySQL药房管理”则框定了数据模型的务实性——不用MongoDB存非结构化说明书,不用Redis缓存热销药排名,就用一张 yaopin 表存药品基础信息,一张 ruku 表记入库流水,一张 chuku 表记出库流水,外加 yonghu 和 quanxian 表做权限隔离,所有字段名直白如“shuliang”(数量)、“danjia”(单价)、“caozuo_shijian”(操作时间),连没学过SQL的药师都能对着Excel理解字段含义。
所以,这不是一套“过时”的系统,而是一套拒绝技术幻觉、直面基层需求的务实方案。它不追求代码行数少,但追求每一行都可读、可改、可验证;它不标榜架构先进,但保证三个月后换人维护时,新来的实习生花半天就能搞懂“为什么修改药品信息要同时更新 yaopin 表和 ruku/chuku 关联记录”。接下来,我们就一层层拆开这个看似简单的系统,看看它如何用最朴素的技术,解决最实在的问题。
2. 系统架构与模块设计:B/S结构下的“药房数字账本”逻辑
2.1 整体架构:为什么坚持用JSP+Servlet+JDBC而不引入框架?
先说结论:不是不能用Spring MVC,而是没必要。 这个系统的请求特征非常明确——95%以上的HTTP请求都是同步表单提交(比如点击“新增入库”按钮,跳转到 add_ruku.jsp 提交数据),响应内容90%是HTML页面重定向或简单JSON提示(如“添加成功”),几乎没有异步加载、长连接、消息推送等复杂交互。在这种场景下,引入Spring MVC带来的收益(如依赖注入、AOP日志)远小于其成本(学习曲线、配置复杂度、部署包体积增大、Tomcat内存占用上升)。
我们来看实际对比:
- 纯JSP+Servlet方案:登录校验逻辑写在 logincheck.jsp 里,用 request.getParameter() 获取用户名密码,调用 DBO.java 的 checkUser() 方法查数据库,返回 true 就 response.sendRedirect(“main.jsp”),false 就 forward 回 login.jsp 并带错误提示。整个流程15行代码搞定,学生调试时打断点一眼看到参数怎么传、结果怎么判、跳转去哪。
- Spring MVC方案:需要配置 DispatcherServlet、编写 LoginController 类、定义 @RequestMapping、注入 UserService Bean、处理 ModelMap 返回视图名……光是让项目跑起来,就得配 web.xml、spring-mvc.xml、applicationContext.xml 三个XML文件,新手容易卡在“404找不到login.do”上一整天。
更关键的是可维护性。我在某职业院校带毕业设计时发现,用Spring Boot的学生,有近三分之一最终交的代码里 Controller 层全是复制粘贴,Service 层空着没写,DAO 层直接用 MyBatis Generator 自动生成——他们掌握了工具,却没理解“用户提交的数据如何变成数据库里的一行记录”这个本质过程。而用这套JSP系统的学生,必须亲手写 String sql = "INSERT INTO ruku (yaopin_id, shuliang, danjia, zongjia, caozuo_yuan, caozuo_shijian) VALUES (?, ?, ?, ?, ?, ?)";,必须手动 setParameter() 给每个问号赋值,必须 catch SQLException 处理插入失败。这个“笨功夫”,恰恰是建立数据库思维和Web请求生命周期认知的必经之路。
所以,架构选择背后的逻辑很朴素:教学目标不是培养框架搬运工,而是培养能读懂业务、写出正确SQL、理解HTTP状态码的初级开发者。 JSP+JDBC就像一把没有保险的猎枪——它不安全(需防SQL注入),不优雅(Java代码混在HTML里),但你扣动扳机的每一刻,都清楚子弹从哪来、往哪飞、打中什么。这种“可控的原始感”,对入门者反而是最好的启蒙。
2.2 模块划分:五个核心业务域如何协同构成“药品生命线”
系统功能看似琐碎,实则围绕一条主线展开:药品从入库到出库的全链路追踪。 我们把所有JSP页面和Java类按业务域归类,就能看清这张“生命线”是如何被数字化的:
| 模块名称 | 核心JSP页面 | 关键Java类 | 解决的核心问题 | 数据表关联 |
|---|---|---|---|---|
| 用户与权限 | login.jsp, yonghu/list.jsp, yonghu/add.jsp | UserDAO.java, QuanxianDAO.java | 谁能做什么?不同角色看到的菜单、能操作的按钮有何不同? | yonghu(用户表)、quanxian(权限表)、yonghu_quanxian(中间关联表) |
| 药品主数据 | yaopin/list.jsp, yaopin/add.jsp, yaopin/edit.jsp | YaopinDAO.java, FenleiDAO.java | 药品有哪些?分类是否清晰?规格、单位、有效期怎么管? | yaopin(药品表)、fenlei(分类表)、yaopin_fenlei(多对多关联) |
| 入库管理 | ruku/list.jsp, ruku/add.jsp, ruku/detail.jsp | RukuDAO.java, RukuDetailDAO.java | 新药怎么进仓?供应商信息、批次号、采购价、入库时间如何登记? | ruku(入库主表)、ruku_detail(入库明细表,含药品ID、数量、单价) |
| 出库管理 | chuku/list.jsp, chuku/add.jsp, chuku/detail.jsp | ChukuDAO.java, ChukuDetailDAO.java | 药品怎么卖出?是零售、科室领用还是报损?流向哪里? | chuku(出库主表)、chuku_detail(出库明细表) |
| 基础配置与公告 | gonggao/list.jsp, gonggao/add.jsp, baobiao/stock.jsp | GonggaoDAO.java, YaoFangConfigDAO.java | 药房名称、地址、联系电话怎么维护?重要通知如何发布?库存预警阈值设多少? | gonggao(公告表)、yaofang_config(药房配置表) |
你会发现,所有模块都指向同一个底层逻辑:以药品ID(yaopin_id)为纽带,串联起每一次入库、出库、库存变动。 比如在 yaopin/list.jsp 页面点击“查看库存”,后台执行的其实是这样的SQL:
SELECT
y.id, y.mingcheng, y.guige, y.danwei,
COALESCE((SELECT SUM(rd.shuliang) FROM ruku_detail rd JOIN ruku r ON rd.ruku_id=r.id WHERE rd.yaopin_id=y.id AND r.status='已审核'), 0) AS ruku_total,
COALESCE((SELECT SUM(cd.shuliang) FROM chuku_detail cd JOIN chuku c ON cd.chuku_id=c.id WHERE cd.yaopin_id=y.id AND c.status='已审核'), 0) AS chuku_total,
COALESCE((SELECT SUM(rd.shuliang) FROM ruku_detail rd JOIN ruku r ON rd.ruku_id=r.id WHERE rd.yaopin_id=y.id AND r.status='已审核'), 0) -
COALESCE((SELECT SUM(cd.shuliang) FROM chuku_detail cd JOIN chuku c ON cd.chuku_id=c.id WHERE cd.yaopin_id=y.id AND c.status='已审核'), 0) AS current_stock
FROM yaopin y;
这段SQL没有用任何高级特性,就是两个子查询分别算总入库量和总出库量,再相减得当前库存。但它精准回答了药师最关心的问题:“这个药到底还有多少盒?”——而且答案来自真实业务单据,不是靠定时任务刷新的缓存。这种“所见即所得”的确定性,正是药房系统最需要的品质。
2.3 权限控制机制:细粒度到按钮级的“角色-功能”映射
很多初学者以为权限控制就是“管理员能看到所有菜单,操作员只能看部分菜单”,这套系统做得更细——它实现了按钮级权限控制。你打开 menu.jsp,会看到这样的代码片段:
<% if(session.getAttribute("userRole").equals("admin")) { %>
<li><a href="yonghu/list.jsp">用户管理</a></li>
<% } %>
<% if(session.getAttribute("userRole").equals("admin") || session.getAttribute("userRole").equals("caozuo")) { %>
<li><a href="ruku/add.jsp">新增入库</a></li>
<li><a href="chuku/add.jsp">新增出库</a></li>
<% } %>
但真正的权限判断不在这里,而在每个具体操作页面的顶部。比如 ruku/add.jsp 开头就有:
<%@ page import="com.dao.QuanxianDAO" %>
<%
String userId = (String)session.getAttribute("userId");
QuanxianDAO qxDao = new QuanxianDAO();
boolean canAddRuku = qxDao.hasPermission(userId, "ruku_add"); // 查询该用户是否有“入库新增”权限
if(!canAddRuku) {
response.sendRedirect("../no_permission.jsp");
return;
}
%>
这个 hasPermission() 方法在 QuanxianDAO.java 中实现,它查询的是 quanxian 表中 code 字段为 ‘ruku_add’ 的权限记录,并通过中间表 yonghu_quanxian 判断当前用户是否拥有该权限。这意味着:
- 管理员可以给某个操作员单独开通“入库新增”权限,但不给他“用户管理”权限;
- 可以设置某个账号只能录入药品信息(yaopin_add),但不能删除(yaopin_delete);
- 权限变更无需重启服务器,改数据库即可生效。
我曾见过某医院药房因权限设置不当导致的问题:实习药师误点了“删除药品”按钮,把正在使用的降压药从系统里删了,幸好有操作日志(chuku_log 表记录每次出库的详细操作人、时间、IP),才快速定位并恢复。所以,这套系统在 DBO.java 里特意加了日志记录方法:
public static void logAction(String userId, String actionType, String targetId, String detail) {
String sql = "INSERT INTO action_log (user_id, action_type, target_id, detail, create_time) VALUES (?, ?, ?, ?, NOW())";
// 执行插入...
}
所有关键操作(增删改药品、出入库、修改密码)都会调用此方法。这不是锦上添花的功能,而是药房系统合规性的底线——当药监部门来检查时,“谁能做什么、什么时候做的、做了什么”必须有据可查。
3. 核心功能实现详解:从登录验证到库存计算的完整链路
3.1 登录与会话管理:如何用最简方式保障基础安全
登录看似简单,却是整个系统安全的第一道闸门。这套系统没有用Shiro或Spring Security,而是用最原始但也最可控的方式实现:
第一步:表单提交与参数获取
login.jsp 的表单是标准的POST:
<form action="logincheck.jsp" method="post">
<input type="text" name="username" placeholder="用户名" required>
<input type="password" name="password" placeholder="密码" required>
<button type="submit">登录</button>
</form>
注意,这里没有前端JS加密(如MD5),因为JSP运行在服务端,密码明文传输的风险由HTTPS解决——但在本地开发时,我们默认走HTTP,所以必须在后端做处理。
第二步:密码校验与SQL注入防护
logincheck.jsp 的核心逻辑:
<%@ page import="com.dao.UserDAO" %>
<%
String username = request.getParameter("username");
String password = request.getParameter("password");
// 关键!使用PreparedStatement防止SQL注入
UserDAO userDao = new UserDAO();
boolean isValid = userDao.checkLogin(username, password); // 内部用预编译语句
if(isValid) {
// 查询用户完整信息(含角色)
User user = userDao.getUserByUsername(username);
session.setAttribute("userId", user.getId());
session.setAttribute("userName", user.getUsername());
session.setAttribute("userRole", user.getRole()); // "admin" 或 "caozuo"
session.setAttribute("loginTime", new Date());
response.sendRedirect("main.jsp");
} else {
request.setAttribute("errorMsg", "用户名或密码错误");
request.getRequestDispatcher("login.jsp").forward(request, response);
}
%>
UserDAO.java 中的 checkLogin() 方法:
public boolean checkLogin(String username, String password) {
String sql = "SELECT COUNT(*) FROM yonghu WHERE username=? AND password=? AND status='启用'";
try (Connection conn = DBO.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, username); // 预编译参数绑定
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();
return rs.next() && rs.getInt(1) > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
这里有两个重点:
1. 预编译语句(PreparedStatement) 是防SQL注入的黄金标准。如果有人在用户名框输入 ' OR '1'='1,预编译会把它当作字符串字面值处理,不会拼接到SQL里变成 WHERE username='' OR '1'='1' AND password=...;
2. 密码未加密存储 是本系统的妥协点(默认密码admin/admin明文存库),但实际部署时,你必须在UserDAO.addUser()方法里加入MD5加密:
// 新增用户时
String encryptedPwd = DigestUtils.md5Hex(password); // Apache Commons Codec
String sql = "INSERT INTO yonghu (username, password, role, status) VALUES (?, ?, ?, ?)";
pstmt.setString(2, encryptedPwd); // 存加密后的密码
然后 logincheck.jsp 的校验逻辑也要同步改为 pstmt.setString(2, DigestUtils.md5Hex(password));。这个改造只需5分钟,却是安全性的分水岭。
第三步:会话超时与强制登出
main.jsp 顶部有会话校验:
<%
if(session.getAttribute("userId") == null) {
response.sendRedirect("login.jsp");
return;
}
%>
而 logout.jsp 更彻底:
<%
session.invalidate(); // 彻底销毁会话
response.sendRedirect("login.jsp");
%>
为什么不用 session.removeAttribute()?因为药房电脑常共用,前一个用户退出后,后一个用户可能直接按浏览器后退键回到上一个用户的main.jsp页面——如果只是清除属性,session对象还在,session.getAttribute("userId") 可能返回旧值。invalidate() 才是真正“注销”的正确姿势。
3.2 药品入库全流程:从采购单到库存更新的事务一致性
入库是药房最频繁的操作,也是最容易出错的环节。系统用一个典型的“采购入库”场景来演示完整链路:
场景:药房采购了3种药品——阿莫西林胶囊(10盒,单价12.5元)、复方甘草片(20瓶,单价8.0元)、电子血压计(5台,单价180元)。需要录入系统,生成唯一入库单号,更新库存,并留下可追溯记录。
操作路径:main.jsp → ruku/add.jsp → 提交 → ruku/success.jsp
关键实现细节:
1. 入库单号自动生成:在 ruku/add.jsp 提交前,JavaScript生成格式为 RK20240520001 的单号(RK+日期+三位序号)。后端Java类 RukuDAO.java 在 insertRuku() 方法里会校验该单号是否已存在,避免重复提交。
2. 明细行动态添加:add.jsp 使用jQuery动态添加药品明细行,每行包含药品下拉框(从yaopin表查出)、数量、单价、小计(数量×单价自动计算)。提交时,所有明细数据以数组形式传到后台:
// JS收集明细数据
var details = [];
$("table#detailTable tbody tr").each(function() {
var row = $(this);
details.push({
yaopinId: row.find("select[name='yaopin_id']").val(),
shuliang: row.find("input[name='shuliang']").val(),
danjia: row.find("input[name='danjia']").val()
});
});
$.post("ruku_do_add.jsp", {details: JSON.stringify(details), ...}, ...);
- 事务性入库:ruku_do_add.jsp 是核心控制器,它必须保证“主表插入”和“明细表插入”要么都成功,要么都失败:
Connection conn = null;
PreparedStatement pstmtMain = null;
PreparedStatement pstmtDetail = null;
try {
conn = DBO.getConnection();
conn.setAutoCommit(false); // 关闭自动提交,开启事务
// 插入主表
String sqlMain = "INSERT INTO ruku (ruku_no, gys_name, caozuo_yuan, caozuo_shijian, status) VALUES (?, ?, ?, NOW(), '待审核')";
pstmtMain = conn.prepareStatement(sqlMain, Statement.RETURN_GENERATED_KEYS);
pstmtMain.setString(1, rukuNo);
pstmtMain.setString(2, gysName);
pstmtMain.setString(3, userId);
pstmtMain.executeUpdate();
// 获取自增主键(ruku_id)
ResultSet rs = pstmtMain.getGeneratedKeys();
rs.next();
int rukuId = rs.getInt(1);
// 批量插入明细
String sqlDetail = "INSERT INTO ruku_detail (ruku_id, yaopin_id, shuliang, danjia, zongjia) VALUES (?, ?, ?, ?, ?)";
pstmtDetail = conn.prepareStatement(sqlDetail);
for(DetailItem item : details) {
pstmtDetail.setInt(1, rukuId);
pstmtDetail.setInt(2, item.getYaopinId());
pstmtDetail.setInt(3, item.getShuliang());
pstmtDetail.setDouble(4, item.getDanjia());
pstmtDetail.setDouble(5, item.getShuliang() * item.getDanjia());
pstmtDetail.addBatch();
}
pstmtDetail.executeBatch();
conn.commit(); // 提交事务
request.setAttribute("msg", "入库单创建成功,单号:" + rukuNo);
} catch (SQLException e) {
conn.rollback(); // 出错回滚
request.setAttribute("errorMsg", "入库失败:" + e.getMessage());
} finally {
// 关闭资源...
}
这个事务处理确保了数据一致性:如果明细插入到一半数据库断连,主表那条记录也会被回滚,不会留下“有头无尾”的脏数据。
第四步:库存实时更新
很多人以为入库后库存就自动增加了,其实系统里有个关键设计:入库单默认状态是“待审核”,只有审核通过后才更新库存。 这符合药房实际流程——采购员录完单,要等药师核对实物无误后,再在 ruku/list.jsp 页面点击“审核”按钮,触发 updateStockOnRukuApprove() 方法:
public void updateStockOnRukuApprove(int rukuId) {
String sql = "UPDATE yaopin y JOIN ruku_detail rd ON y.id=rd.yaopin_id " +
"SET y.kucun = y.kucun + rd.shuliang " +
"WHERE rd.ruku_id = ?";
// 执行更新...
}
这样设计的好处是:
- 避免录入错误导致库存虚高(比如把10盒输成100盒);
- 审核环节可留痕(chuku_log 表记录谁在何时审核了哪张单);
- 为后续“退货”、“冲红”等逆向操作留出空间。
3.3 库存计算与报表生成:如何让“当前库存”真正可信
药房最怕的不是库存少,而是“系统显示有50盒,货架上却只剩3盒”。这套系统用两种方式保障库存数字的可靠性:
方式一:基于单据的实时计算(推荐用于查询)
如前所述,yaopin/list.jsp 的库存查询用的是子查询聚合,优点是绝对准确(直接算单据),缺点是大数据量时慢。针对药房日均单据<100张的场景,完全够用。
方式二:库存快照表(用于报表导出)
系统在 baobiao/stock.jsp 页面提供“库存盘点报表”,它不实时计算,而是读取 kucun_snapshot 表:
CREATE TABLE kucun_snapshot (
id INT PRIMARY KEY AUTO_INCREMENT,
yaopin_id INT NOT NULL,
kucun INT NOT NULL,
snapshot_time DATETIME DEFAULT CURRENT_TIMESTAMP,
operator VARCHAR(50),
remark TEXT
);
这个表由管理员定期(如每月1日)手动触发生成:
<!-- baobiao/snapshot.jsp -->
<%
KucunSnapshotDAO dao = new KucunSnapshotDAO();
dao.generateSnapshot(session.getAttribute("userId").toString());
out.println("盘点快照生成成功!");
%>
KucunSnapshotDAO.generateSnapshot() 方法会执行:
String sql = "INSERT INTO kucun_snapshot (yaopin_id, kucun, operator, remark) " +
"SELECT y.id, y.kucun, ?, '月度盘点' FROM yaopin y";
这样,报表导出时就不用实时聚合,直接查快照表,速度快且稳定。更重要的是,它提供了历史对比——你可以查“上月快照”和“本月快照”,一眼看出哪些药品库存异常波动。
报表导出功能:baobiao/export_stock.jsp 使用Apache POI生成Excel:
HSSFWorkbook workbook = new HSSFWorkbook();
HSSFSheet sheet = workbook.createSheet("库存报表");
// 设置表头
HSSFRow headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("药品名称");
headerRow.createCell(1).setCellValue("规格");
headerRow.createCell(2).setCellValue("单位");
headerRow.createCell(3).setCellValue("当前库存");
// 填充数据...
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment; filename=stock_report_" + new SimpleDateFormat("yyyyMMdd").format(new Date()) + ".xls");
workbook.write(response.getOutputStream());
注意,这里用的是HSSF(Excel 97-2003格式),而非XSSF(xlsx),因为POI的XSSF在Tomcat 7.0环境下容易内存溢出,而HSSF足够满足药房报表需求(行数<65536)。
4. 数据库设计与脚本解析:MySQL中的药房业务建模
4.1 核心数据表结构:从ER图到字段命名的业务思考
数据库脚本 jspyyygl.sql 是整个系统的基石。我们不逐行解读DDL语句,而是聚焦三个关键表的设计哲学:
表1:yaopin(药品主表)——为什么用“拼音首字母”做编码规则?
CREATE TABLE yaopin (
id INT PRIMARY KEY AUTO_INCREMENT,
code VARCHAR(20) NOT NULL COMMENT '药品编码,如AMLXL001',
mingcheng VARCHAR(100) NOT NULL COMMENT '药品名称',
pinyin_code VARCHAR(20) COMMENT '拼音首字母,如"AMLXL"',
guige VARCHAR(50) COMMENT '规格,如"0.25g*24粒/盒"',
danwei VARCHAR(20) COMMENT '单位,如"盒"、"瓶"、"台"',
kucun INT DEFAULT 0 COMMENT '当前库存',
min_kucun INT DEFAULT 0 COMMENT '最低库存预警值',
shengchan_qi DATE COMMENT '生产日期',
youxiao_qi DATE COMMENT '有效期至',
chandi VARCHAR(50) COMMENT '产地',
shengchan_changjia VARCHAR(100) COMMENT '生产厂家',
jibie VARCHAR(20) COMMENT '药品级别,如"处方药"、"非处方药"',
status VARCHAR(10) DEFAULT '启用' COMMENT '状态'
);
code 字段不是UUID或自增ID,而是人工维护的编码(如阿莫西林胶囊=AMLXL001)。原因很实际:药师在盘点时,拿着纸质清单念“AMLXL001”,比念“id=12345”快得多;药房ERP系统未来对接时,这个编码也是行业通用标识。而 pinyin_code 字段(阿莫西林→AMLXL)则是为模糊搜索准备的——在 yaopin/list.jsp 的搜索框输入“aml”,SQL就能匹配 pinyin_code LIKE 'aml%',比全文索引更轻量。
表2:ruku(入库主表)与 ruku_detail(入库明细表)——为什么要拆分主从?
这是典型的“一对多”关系建模。主表 ruku 记录单次采购的整体信息:
CREATE TABLE ruku (
id INT PRIMARY KEY AUTO_INCREMENT,
ruku_no VARCHAR(20) NOT NULL UNIQUE COMMENT '入库单号,如RK20240520001',
gys_name VARCHAR(100) COMMENT '供应商名称',
caozuo_yuan VARCHAR(50) COMMENT '操作员',
caozuo_shijian DATETIME COMMENT '操作时间',
status ENUM('待审核','已审核','已作废') DEFAULT '待审核',
remark TEXT COMMENT '备注'
);
明细表 ruku_detail 记录每一项药品的入库详情:
CREATE TABLE ruku_detail (
id INT PRIMARY KEY AUTO_INCREMENT,
ruku_id INT NOT NULL COMMENT '关联入库单ID',
yaopin_id INT NOT NULL COMMENT '关联药品ID',
shuliang INT NOT NULL COMMENT '数量',
danjia DECIMAL(10,2) COMMENT '单价',
zongjia DECIMAL(10,2) COMMENT '总价',
batch_no VARCHAR(50) COMMENT '批次号',
youxiao_qi DATE COMMENT '有效期至',
FOREIGN KEY (ruku_id) REFERENCES ruku(id) ON DELETE CASCADE,
FOREIGN KEY (yaopin_id) REFERENCES yaopin(id)
);
拆分的好处显而易见:
- 一张入库单可以包含N种药品,明细表行数可变;
- 删除主表记录时,ON DELETE CASCADE 自动清理所有明细,避免孤儿数据;
- 查询某张单据的所有药品,只需 SELECT * FROM ruku_detail WHERE ruku_id=123,效率极高。
表3:yonghu(用户表)与 quanxian(权限表)——RBAC模型的极简实现
CREATE TABLE yonghu (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) COMMENT 'MD5加密后的密码',
realname VARCHAR(50) COMMENT '真实姓名',
role VARCHAR(20) COMMENT '角色,如admin/caozuo',
phone VARCHAR(20),
email VARCHAR(50),
status ENUM('启用','禁用') DEFAULT '启用'
);
CREATE TABLE quanxian (
id INT PRIMARY KEY AUTO_INCREMENT,
code VARCHAR(50) NOT NULL UNIQUE COMMENT '权限编码,如yaopin_add',
name VARCHAR(100) COMMENT '权限名称,如"药品新增"',
module VARCHAR(20) COMMENT '所属模块,如yaopin'
);
CREATE TABLE yonghu_quanxian (
id INT PRIMARY KEY AUTO_INCREMENT,
yonghu_id INT NOT NULL,
quanxian_id INT NOT NULL,
FOREIGN KEY (yonghu_id) REFERENCES yonghu(id) ON DELETE CASCADE,
FOREIGN KEY (quanxian_id) REFERENCES quanxian(id) ON DELETE CASCADE,
UNIQUE KEY uk_user_perm (yonghu_id, quanxian_id)
);
这个设计放弃了“角色-权限”二级映射(即先定义角色,再给角色赋予权限),而是直接让用户关联权限。为什么?因为药房用户数通常<20人,权限组合有限(管理员=全部权限,操作员=入库+出库+查询),硬套RBAC反而增加复杂度。yonghu.role 字段保留是为了快速判断(如菜单栏显示逻辑),而细粒度控制交给 yonghu_quanxian 表。
4.2 数据库连接配置:DBO.java中的“生命线”参数
所有数据库操作都通过 DBO.java 封装,这是系统的“数据中枢”。它的 getConnection() 方法是关键:
public static Connection getConnection() throws SQLException {
String url = "jdbc:mysql://localhost:3306/jspyyygl?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8";
String username = "root"; // 默认MySQL用户名
String password = "123456"; // 默认MySQL密码
try {
Class.forName("com.mysql.jdbc.Driver"); // MySQL 5.x驱动
// Class.forName("com.mysql.cj.jdbc.Driver"); // MySQL 8.x驱动,需取消注释并更换jar包
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return DriverManager.getConnection(url, username, password);
}
实操注意事项:
- URL中的 serverTimezone=GMT%2B8 是必须的,否则MySQL 5.7+版本会报时区错误;
- 驱动类名差异:MySQL 5.x 用 com.mysql.jdbc.Driver,MySQL 8.x 用 com.mysql.cj.jdbc.Driver,且8.x驱动jar包名是 mysql-connector-java-8.0.xx.jar,而5.x是 mysql-connector-java-5.1.xx-bin.jar。项目包里附带的是5.x驱动,如果你用MySQL 8.x,必须替换jar包并修改Driver类名;
- 密码安全性:生产环境绝不能写死密码!应改为读取外部配置文件:
// 从db.properties读取
Properties props = new Properties();
props.load(new FileInputStream("WEB-INF/db.properties"));
String password = props.getProperty("db.password");
db.properties 文件放在 WEB-INF 目录下,内容为:
db.url=jdbc:mysql://localhost:3306/jspyyygl?...
db.username=root
db.password=your_secure_password
这样,部署时只需改配置文件,无需动Java代码。
4.3 初始化数据脚本:maker.sql中的“种子数据”价值
除了主库脚本 jspyyygl.sql,项目还提供了 maker.sql,它负责插入初始数据:
-- 插入默认管理员
INSERT INTO yonghu (username, password, realname, role, status)
VALUES ('admin', '21232f297a57a5a743894a0e4a801fc3', '系统管理员', 'admin', '启用'); -- MD5(admin)
-- 插入常用药品分类
INSERT INTO fenlei (name, code) VALUES ('抗生素', 'KS'), ('中成药', 'ZCY'), ('医疗器械', 'YLQX');
-- 插入几个示例药品
INSERT INTO yaopin (code, mingcheng, pinyin_code, guige, danwei, kucun, min_kucun)
VALUES
('AMLXL001', '阿莫西林胶囊', 'AMLXL', '0.25g*24粒/盒', '盒', 100, 20),
('FGCTP001', '复方甘草片', 'FGCTP', '0.2g*100片/瓶', '瓶', 200, 50);
这些“种子数据”的作用被很多人低估。它不只是让你系统能跑起来,更是教学示范的活教材:
- 学生看到 pinyin_code 字段值,立刻明白“AMLXL”是怎么来的;
- 看到 fenlei 表的插入语句,就知道分类编码规则(KS=抗生素);
- yaopin 表的 min_kucun 字段设为20,结合后续的库存预警功能(baobiao/warning.jsp 查询 kucun < min_kucun 的药品),学生能直观理解“最低库存”的业务含义。
所以,导入 maker.sql 不是可选项,而是理解系统业务逻辑的起点。
5. 部署与调试实战:从MyEclipse到Tomcat 7.0的完整落地指南
5.1 开发环境搭建:MyEclipse配置的“避坑三原则”
MyEclipse 2014/2015 是这套系统最适配的IDE,但新手常在环境配置上卡住。根据我带过的37个毕业设计小组的经验,总结出三个必须遵守的原则:
原则一:JDK版本必须为1.7或1.8
Tomcat 7.0 官方支持的最高JDK版本是1.8。如果你装了JDK 11或17,MyEclipse会报错 Unsupported major.minor version 55.0(JDK 11对应字节码版本55)。解决方案:
- 下载 JDK 1.8(推荐 Oracle JDK 1.8.0_202 或 OpenJDK 8u292);
- 在MyEclipse中:Window → Preferences → Java → Installed JREs → Add → Standard VM → 指向JDK 1.8安装目录;
- 右键项目 → Properties → Java Build Path → Libraries → Remove old JRE → Add Library → JRE System Library → Workspace default JRE(即JDK 1.8)。
原则二:Web Project Facets必须匹配Tomcat版本
右键项目 → Properties → MyEclipse → Project Facets:
- Dynamic Web Module 版本选 3.0(Tomcat 7.0对应Servlet 3.0规范);
- Java 版本选 1.7 或 1.8;
- JavaScript Version 选 1.0(JSP不需要高级JS特性)。
如果选错(比如Dynamic Web Module选2.5),部署时会报 Servlet API not found 错误。
原则三:MySQL驱动JAR包必须放在WEB-INF/lib下
项目包里的 mysql-connector-java-5.1.47-bin.jar 必须复制到 WebRoot/WEB-INF/lib/ 目录。常见错误:
- 把jar包放在 src/ 目录下——编译时能找到,但运行时Tomcat的ClassLoader找不到;
- 放在 WebRoot/WEB-INF/ 根目录下——缺少lib子目录,Tomcat不扫描;
- 用MyEclipse的Build Path添加——这只会让编译通过,但不会打包到WAR里。
正确做法:直接拖拽jar包到 WebRoot/WEB-INF/lib/ 文件夹,MyEclipse会自动识别为Web库。
5.2 Tomcat 7.0部署全流程:五步完成“从零到登录”
步骤1:配置Tomcat服务器
- MyEclipse菜单:Window → Preferences → MyEclipse → Servers → Tomcat → Tomcat 7.x → Browse → 选择Tomcat 7.0安装目录(如 D:\apache-tomcat-7.0.109);
- 点击Apply,确保Status显示“OK”。
步骤2:创建数据库并导入脚本
- 启动MySQL服务(如通过XAMPP控制面板);
- 用phpMyAdmin或Navicat新建数据库 jspyyygl,字符集选 utf8mb4_unicode_ci;
- 导入 jspyyygl.sql(创建表结构);
- 再导入 maker.sql(插入初始数据)。
步骤3:修改数据库连接参数
打开 src/com/dao/DBO.java,找到 getConnection() 方法,修改以下三行:
String url = "jdbc:mysql://localhost:3306/jspyyygl?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8";
String username = "root"; // 你的MySQL用户名
String password = "123456"; // 你的MySQL密码
注意:如果MySQL root密码不是123456,请务必修改。常见错误是忘记改密码,导致登录时一直提示“数据库连接失败”。
步骤4:部署项目到Tomcat
- 右键项目 → MyEclipse → Add and Remove Projects → 选择你的项目 → Add → Finish;
- MyEclipse底部Servers标签页,右键Tomcat 7.x → Start;
- 控制台输出 INFO: Server startup in XXX ms 即启动成功。
步骤5:浏览器访问与首次登录
- 打开浏览器,输入 http://localhost:8080/YourProjectName/login.jsp;
- 输入默认账号 admin / admin;
- 成功进入 main.jsp,看到左侧菜单栏,证明部署完成。
常见启动失败排查:
- 浏览器显示 404:检查项目名是否正确(URL中的 YourProjectName 必须与MyEclipse里项目名一致);
- 显示 HTTP Status 500:查看MyEclipse Console窗口的红色错误日志,90%是 ClassNotFoundException(驱动jar包没放对位置)或 SQLException(数据库连接参数错);
- 登录页空白:检查 login.jsp 是否被意外修改,或 WebRoot/ 目录下是否有同名的 login.jsp.bak 文件干扰(项目包里就有 .bak 文件,记得删掉)。
5.3 功能调试技巧:如何快速定位JSP+Java混合问题
JSP页面里混着HTML、Java代码、EL表达式,调试比纯Java难。分享三个实战技巧:
技巧1:用out.println()做“土法”断点
在 logincheck.jsp 关键位置插入:
<%
String username = request.getParameter("username");
out.println("DEBUG: username=" + username + "<br>"); // 输出到页面
// 后续逻辑...
%>
这样,如果页面显示 DEBUG: username=admin,说明参数获取正常;如果没显示,说明表单提交路径错了。
技巧2:查看Tomcat日志定位SQL错误
当 ruku_do_add.jsp 报错时,不要只看浏览器500页面,去 tomcat/logs/catalina.out 文件末尾找:
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException:
Column 'yaopin_id' cannot be null
这说明 ruku_detail 表的 yaopin_id 字段为空,问题出在前端JS没正确获取下拉框选中的值。
技巧3:用Firebug或Chrome DevTools检查AJAX请求
如果 yaopin/list.jsp 的搜索功能失效,按F12打开开发者工具 → Network → XHR,然后在搜索框输入关键词并回车,看是否发出请求、返回什么JSON、状态码是多少(200表示成功,500表示后台异常)。这能快速区分问题是前端JS写错了,还是后端Java逻辑有问题。
6. 常见问题与独家避坑指南:那些没人告诉你的“药房系统陷阱”
6.1 典型问题速查表:从环境配置到业务逻辑的高频故障
| 问题现象 | 可能原因 | 解决方案 | 经验等级 |
|---|---|---|---|
| 登录页空白,无任何错误 | login.jsp 被重命名为 login.jsp.bak,或 WebRoot/ 下存在同名备份文件 |
删除所有 .bak 文件(如 login.jsp.bak, main.jsp.bak),确保 login.jsp 是唯一入口文件 |
★☆☆☆☆(新手必踩) |
| 登录提示“用户名或密码错误”,但确认输入正确 | DBO.java 中数据库密码未修改,或MySQL root密码不是123456 |
检查 DBO.java 的 username 和 password 字段;用命令行 mysql -u root -p 测试能否登录MySQL |
★★☆☆☆ |
| 新增药品后,列表页不显示 | yaopin 表的 status 字段默认为’启用’,但插入SQL漏写了该字段 |
检查 YaopinDAO.addYaopin() 方法,确保INSERT语句包含 status 字段:VALUES (..., '启用') |
★★★☆☆ |
| 入库单审核后,药品库存没增加 | ruku 表的 status 字段值不是’已审核’(可能是大小写错误,如’已审核’ vs ‘已审核 ‘) |
在phpMyAdmin中查 ruku 表,确认 status 值精确等于’已审核’;修改 RukuDAO.updateStatus() 方法,用 status='已审核' 而非 status='已审核 ' |
★★★★☆ |
| 导出Excel报表中文乱码 | response.setContentType() 设置错误,或POI写入时未指定编码 |
确保 export_stock.jsp 中:response.setContentType("application/vnd.ms-excel;charset=UTF-8");response.setHeader("Content-Disposition", "attachment; filename=stock_report.xls"); |
★★★☆☆ |
6.2 独家避坑经验:来自真实药房上线的血泪教训
坑1:药品单位混乱导致库存计算错误
某乡镇卫生院上线后发现,系统里“电子血压计”库存是5台,但实际货架上只有2台。排查发现:采购员录入时,在 danwei 字段填了“台”,但领用时在 chuku 表里填了“个”,导致 chuku_detail.shuliang 加了单位后无法与 yaopin.danwei 匹配。
解决方案:在 yaopin 表增加约束:
ALTER TABLE yaopin ADD CONSTRAINT chk_danwei CHECK (danwei IN ('盒','瓶','支','台','袋','支'));
并在所有新增页面(yaopin/add.jsp, ruku/add.jsp)的单位下拉框中,只提供这几个固定选项,禁止手工输入。
坑2:有效期管理形同虚设
系统有 youxiao_qi 字段,但没人检查过期药。直到药监检查,才发现3盒过期阿司匹林还在库存里。
补救措施:在 baobiao/expired.jsp 添加过期药品查询:
SELECT y.*, DATEDIFF(NOW(), y.youxiao_qi) AS days_overdue
FROM yaopin y
WHERE y.youxiao_qi < NOW() AND y.kucun > 0;
并在 main.jsp 顶部加醒目的红色提示:“【警告】检测到3种药品已过期,请立即下架!”
坑3:操作员误删药品主数据
实习药师点了“删除药品”,删掉了正在使用的降压药。虽然有日志,但恢复麻烦。
防御策略:在 yaopin/delete.jsp 前加二次确认,并检查关联:
// 删除前检查是否有入库/出库记录
String sql = "SELECT COUNT(*) FROM ruku_detail rd JOIN ruku r ON rd.ruku_id=r.id " +
"WHERE rd.yaopin_id=? AND r.status='已审核' " +
"UNION ALL " +
"SELECT COUNT(*) FROM chuku_detail cd JOIN chuku c ON cd.chuku_id=c.id " +
"WHERE cd.yaopin_id=? AND c.status='已审核'";
// 如果任一COUNT>0,禁止删除,提示“该药品已有业务单据,不可删除”
坑4:Tomcat内存溢出导致报表导出失败
导出1000行库存报表时,Tomcat报 java.lang.OutOfMemoryError: Java heap space。
调优方案:修改 tomcat/bin/catalina.bat(Windows)或 catalina.sh(Linux),在 set JAVA_OPTS= 行后添加:
set JAVA_OPTS=%JAVA_OPTS% -Xms512m -Xmx1024m -XX:MaxPermSize=256m
将最大堆内存从默认的512MB提升到1024MB。
6.3 功能扩展建议:让系统从“能用”走向“好用”
这套系统作为课程设计足够优秀,但若真要部署到药房,还需三个关键增强:
增强1:药品效期预警邮件
每天凌晨2点,自动扫描 yaopin 表中 youxiao_qi 在30天内到期的药品,用JavaMail发送邮件给管理员。代码框架:
// 在Quartz定时任务中
List<Yaopin> expiringDrugs = yaopinDao.getExpiringInDays(30);
if(!expiringDrugs.isEmpty()) {
sendEmail("药房效期预警", "以下药品即将过期:" + expiringDrugs.toString());
}
增强2:扫码入库/出库
在 ruku/add.jsp 和 chuku/add.jsp 页面增加二维码扫描按钮,调用手机摄像头扫描药品包装上的条形码(如国药准字H12345678),自动填充 yaopin_id。前端用 html5-qrcode 库,后端通过条形码查 yaopin.barcode 字段。
增强3:移动端适配
用Bootstrap重构所有JSP页面,让 main.jsp 在手机浏览器也能流畅操作。重点适配 chuku/add.jsp——药师在药架旁用手机扫描出库,比回办公室用电脑更快。
最后分享一个小技巧:每次功能上线前,我都会让药房的药师用“最笨的方法”测试——比如让他连续录入50张入库单,中间不断刷新页面、关掉浏览器再打开、用不同账号登录切换。系统真正的稳定性,不在压力测试报告里,而在药师说“这系统比我以前的手工账本还顺手”那一刻。 这套JSP系统或许不够酷,但它把“让药房工作更简单”这件事,做到了极致。
简介:一套可直接运行的药店药品进销存管理系统,基于Java Web技术栈开发,前端用JSP实现页面交互,后端逻辑由Java类处理,数据存储采用MySQL数据库,开发环境为MyEclipse,支持Tomcat 7.0部署。系统提供完整的药品生命周期管理功能:药品信息的添加、修改、删除和查询;药品分类维护;多角色用户体系(管理员、操作员等)及对应权限控制;药房基础资料配置;药品入库、出库登记与记录追溯;公告信息发布与管理。所有页面均配套JSP文件,如登录页login.jsp、主界面main.jsp、出入库操作页(ruku/、chuku/目录下)、药品管理yaopin/、用户管理yonghu/等;后台Java代码按包结构组织在src/com/路径下;数据库脚本jspyyygl.sql已准备就绪,导入即可使用;默认管理员账号admin/admin,数据库连接参数在DBO.java中配置。项目目录结构规范,包含WebRoot、WEB-INF、CSS样式、图片资源、业务JSP页面及配置文件,适合教学实践、课程设计或小型药房信息化快速落地。
更多推荐


所有评论(0)