本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套可直接运行的药店药品进销存管理系统,基于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 表记出库流水,外加 yonghuquanxian 表做权限隔离,所有字段名直白如“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), ...}, ...);
  1. 事务性入库: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.71.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.javausernamepassword 字段;用命令行 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.jspchuku/add.jsp 页面增加二维码扫描按钮,调用手机摄像头扫描药品包装上的条形码(如国药准字H12345678),自动填充 yaopin_id。前端用 html5-qrcode 库,后端通过条形码查 yaopin.barcode 字段。

增强3:移动端适配
用Bootstrap重构所有JSP页面,让 main.jsp 在手机浏览器也能流畅操作。重点适配 chuku/add.jsp——药师在药架旁用手机扫描出库,比回办公室用电脑更快。

最后分享一个小技巧:每次功能上线前,我都会让药房的药师用“最笨的方法”测试——比如让他连续录入50张入库单,中间不断刷新页面、关掉浏览器再打开、用不同账号登录切换。系统真正的稳定性,不在压力测试报告里,而在药师说“这系统比我以前的手工账本还顺手”那一刻。 这套JSP系统或许不够酷,但它把“让药房工作更简单”这件事,做到了极致。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套可直接运行的药店药品进销存管理系统,基于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页面及配置文件,适合教学实践、课程设计或小型药房信息化快速落地。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐