简介:一套基于SSM框架的药房管理系统毕业设计源码,面向计算机专业毕业生与需要项目实战的Java学习者。项目获得导师认可,评审分达98分,所有源码均经过本地编译运行与严格调试,可直接部署。系统难度适中,涵盖前端页面、后端逻辑与数据库脚本,适合作为毕设参考、功能扩展或框架实践基础。资源包为zip压缩包,共1222个文件,约19.56MB,包含Java源码、JSP动态页面、CSS样式、JavaScript脚本、HTML页面、图片资源、XML及properties等配置文件,以及SQL数据库脚本,结构清晰,便于按模块查阅与二次开发。目前已有174人学习或下载,说明这套项目源码在毕业设计场景中具有一定参考价值。借助完整包体,可快速理解药房管理系统的前后端交互、数据库表设计及SSM框架整合方式,减少从零搭建的重复工作,便于快速启动项目并扩展功能。
1. 基于SSM框架的药房管理系统,值得花三天时间跑通并讲透的毕设选题
去社区药店买过药都见过那些场景:店员在电脑上输一个药名,库存、价格、效期立刻显示,开单、结账、打印小票一气呵成。毕业设计题目“基于SSM框架的药房管理系统”,要交付的正是这样一套能跑的Web系统——用户登录后按角色操作药品档案、供应商、采购入库、销售出库和库存预警,后端用Spring管理对象和事务,SpringMVC接收页面请求,MyBatis把业务数据落到MySQL。附件里的数据库脚本也不是配角,它是整个系统的地基。
这道题适合两类人来拿。一是需要交毕设的学生,功能量刚好卡在“CRUD之上、微服务之下”,能讲清楚每一步原理;二是学完Java基础想验证自己能不能独立部署项目的人。SSM又是Java后端面试八股文里的常客,把这一套跑通,比背十道框架题都管用。接下来我按这类项目的常见结构,从数据库到登录页把落地路径讲清楚,包括那些让你半夜重启Tomcat的坑。
2. SSM框架为什么撑得起这个药房系统:选型逻辑与模块拆解
2.1 SSM三件套的分工,以及它为什么是面试常客
打开一个SSM项目的pom.xml,你会发现依赖其实就那么几个核心模块。Spring负责两件事:把Controller、Service、SqlSessionFactory这些对象放进容器统一管理,解决到处new对象导致的耦合;再用AOP统一切事务、记操作日志,让业务代码里不掺这些横切逻辑。SpringMVC则只做一件事,把浏览器发来的URL映射到Controller方法,再把Controller返回的视图名或JSON交给前端渲染。MyBatis在SQL层给你留了最大灵活性,你可以手写多表join、自己控制乐观锁,而不是被全自动ORM的映射规则限制住。
为什么毕设题目会选SSM而不是Spring Boot?一个现实原因是很多高校的毕设题库是前几年沉淀下来的,题目名就写着SSM;另一个原因是SSM的配置过程繁琐,反而逼你接触web.xml、DispatcherServlet、数据源装配这些底层细节。Spring Boot把这些全自动了,你反而说不出请求是怎么从URL走到Mapper的。面试时Spring的IoC/AOP、SpringMVC的DispatcherServlet流程、MyBatis的#{}和${}区别都是高频考点,亲手配过一遍SSM,被追问时能拿出实例讲,而不是背概念。
这套组合放在药房管理这种“事务多、表关系清晰、并发量低”的场景里,性价比很高。药房的业务本质是进销存——采购入库、销售出库、库存变化,每一步都涉及多张表的一致性。Spring声明式事务加上MyBatis的灵活SQL,刚好能把这个约束写得干净利落。
2.2 功能模块怎么拆:从店员视角倒推表结构
设计药房系统的第一步不是画用例图,而是把自己代入真实岗位看他们每天干什么。店员收银时看什么?药品的通用名、规格、生产厂家、批准文号、零售价。卖药之前要确认什么?库存够不够、效期过期没有。缺货了怎么办?采购员发起采购单,到货后做入库。晚上下班老板看什么?销售流水、毛利、低库存预警。这些场景直接决定了模块边界和表结构。
按这个思路,药房管理系统通常拆成五个模块组,对应关系如下:
| 模块 | 核心功能 | 对应主要表 | 关键字段 |
|---|---|---|---|
| 基础档案 | 药品字典、供应商、会员管理 | drug_info、supplier、member | drug_info:drug_code、specification、approval_number |
| 采购入库 | 采购单创建审核、到货入库 | purchase_order、purchase_item | purchase_order:order_no、supplier_id、status |
| 销售出库 | 开单、收银、销售明细 | sale_order、sale_item | sale_item:drug_id、quantity、price |
| 库存管理 | 批次库存、效期预警、盘点 | drug_stock、stock_log | drug_stock:batch_no、expire_date、stock_qty |
| 系统管理 | 用户、角色、操作日志 | sys_user、sys_role、sys_log | sys_user:username、password、role_id |
drug_info只管药品的静态档案,价格、规格都在这里;drug_stock管动态库存,同一个药品可能分多个批次,各批次效期不同,不能混在一行里。sale_order和sale_item是一对多关系,一张销售单含多个药品明细,这样的拆法让日结报表和毛利统计都有据可查。stock_log是流水表,每一次出入库都写一条记录,方便对账和排查问题。
有的版本还带一个prescription处方表,用来记录处方药购买时的处方信息。如果题目里没要求,不一定要加;答辩时提到“处方药和非处方药区分管理”的设计思路,反而是加分项。
2.3 角色权限和业务流程怎么串起来
权限模型不需要做得太重。sys_user表里存一个role_id,登录时把用户对象放进Session,再写一个SpringMVC拦截器检查请求路径前缀:/admin/**只允许管理员访问,/cashier/**只允许收银员,/purchase/**只允许采购员。手写拦截器而不是直接上Spring Security,是为了你在答辩时能把Session、Filter、拦截器的关系讲清楚——这是评委喜欢追问的点。
业务流程有三条主线。第一条采购流:采购员登录后新建采购单,选择供应商和药品,提交后由管理员审核;审核通过到货时做入库操作,这一个方法里要同时完成三件事——写采购明细状态、增加drug_stock库存、写stock_log流水,任何一步失败都要回滚。第二条销售流:收银台选药品时实时查库存,下单时校验库存充足再扣减,同样要保证销售单和库存扣减在一个事务里。第三条预警流:查询库存列表时对比drug_stock.stock_qty和drug_info.warn_line,低于预警线就在前端标红,提醒采购员补货。
这三条流程就是答辩时讲业务的主线。模块拆得清楚,表结构才能站得住;表结构站得住,后面数据库脚本导入和系统跑通就顺理成章。
3. 数据库脚本是骨架:表结构、导入顺序与验证三件事
3.1 药房库里的核心表:先看清名字再动手
拿到项目包,我习惯先不看代码,把SQL脚本打开从头到尾扫一遍。药房系统脚本里的表名一般很直观,看到drug_前缀知道是药品相关,purchase_是采购,sale_是销售。先看清楚有哪些表、表之间谁引用谁,后面导入时心里才有底。一张典型的核心表结构大致长这样:
CREATE TABLE `drug_info` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键', `drug_code` VARCHAR(32) NOT NULL COMMENT '药品编码', `drug_name` VARCHAR(128) NOT NULL COMMENT '药品通用名', `specification` VARCHAR(64) DEFAULT NULL COMMENT '规格,如 500mg*24片', `unit` VARCHAR(16) DEFAULT NULL COMMENT '单位:盒/瓶/袋', `manufacturer` VARCHAR(128) DEFAULT NULL COMMENT '生产厂家', `approval_number` VARCHAR(64) DEFAULT NULL COMMENT '批准文号', `retail_price` DECIMAL(10,2) NOT NULL COMMENT '零售价', `warn_line` INT DEFAULT 10 COMMENT '库存预警线', `status` TINYINT DEFAULT 1 COMMENT '1上架 0停用', PRIMARY KEY (`id`), UNIQUE KEY `uk_drug_code` (`drug_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='药品档案表';这段建表SQL里值得注意三个点。drug_code建了唯一索引,业务上作为药品的“身份证号”,导入数据时如果重复会直接报错,这是脚本自带的约束检查。retail_price用DECIMAL(10,2)而不是FLOAT,金额字段浮点运算会出精度问题,这是老工程师的基本功。warn_line是预警线,默认为10,意思是库存少于10盒就在界面标红。字符集统一用utf8mb4,才能存得下生僻字和特殊符号。
库存表的设计也得看一眼。drug_stock通常用drug_id + batch_no + expire_date作为业务唯一键,同一药品不同批次的库存分开记录,效期管理才做得起来。如果只在一个字段里记总数,那效期预警功能就无从谈起,答辩时容易被问住。
3.2 按依赖顺序导入:先建库再建表
导入脚本前先确认两件事:脚本头部有没有CREATE DATABASE语句,以及有没有DROP TABLE IF EXISTS。大多数毕设脚本会在开头先删旧表再建新表,方便重复导入;少部分脚本只包含建表和INSERT数据,需要你手动建库。两种方式按对应场景处理:
# 方式一:脚本自带建库语句,直接重定向导入 mysql -u root -p < pharmacy.sql # 方式二:脚本不含建库语句,先手动建库再source mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS pharmacy DEFAULT CHARACTER SET utf8mb4;" mysql -u root -p pharmacy < pharmacy.sql两种方式的区别在数据库连接目标上。方式一不指定库名,因为脚本内部已经写了USE pharmacy;;方式二先建库,再把pharmacy.sql导入到名为pharmacy的库里。如果你在方式二里忘了指定库名,MySQL会报“No database selected”的错误,导入直接失败,这是新手最容易踩的第一道坎。
还有一个小技巧:如果脚本里的表存在外键依赖,导入时担心顺序问题,可以先在会话里执行SET FOREIGN_KEY_CHECKS=0;临时关闭外键检查,导入完再执行SET FOREIGN_KEY_CHECKS=1;恢复。常见的毕设脚本里表不多,一般不会出这个问题,但这一手能应付表结构更复杂的扩展版本。
3.3 用几条SQL验证导入是否完整
导入成功不代表导入完整。我见过不少同学脚本刷到一半报错,终端提示红字一闪而过没在意,后面项目启动时各种字段找不到。落地到验证这一步,至少跑四条SQL确认数据骨架是完整的:
-- 1. 确认表都建出来了,数量对得上 SHOW TABLES; -- 2. 确认药品档案有数据,不是空库 SELECT COUNT(*) AS drug_cnt FROM drug_info; -- 3. 确认库存表和销售明细表都可读,字段没导丢 SELECT id, drug_id, batch_no, stock_qty, expire_date FROM drug_stock LIMIT 5; -- 4. 确认管理员账号在,密码字段没被截断 SELECT id, username, password, role_id FROM sys_user;SHOW TABLES对照脚本里的建表语句数一遍,缺一张表说明导出或导入过程中断了。COUNT(*)药品数量通常应该在几十条以上,如果脚本里带测试数据,数量太少说明INSERT被跳过。最后查sys_user是为了确认登录入口能进去,密码字段如果显示成e10adc3949ba59abbe56e057f20f883e这类32位字符串,说明是MD5加密后的值,默认密码一般是123456。
验证通过之后,建议你用IDEA或Navicat的“导出数据库脚本”功能,把这个库重新导出一份留作备份。自己导出的脚本是干净的、确定能用的,后续如果改坏了数据,随时能恢复到初始状态。这份自制脚本也可以作为答辩时“数据库设计”环节的交付物。
4. 本地跑通:从解压到登录页的最小命令与配置
4.1 环境版本怎么搭配才不会翻车
SSM是在Maven生态成熟期流行的框架组合,版本匹配有个“别太新”的原则。我见过有人拿JDK 17配Spring 4,启动直接抛IllegalArgumentException,折腾半天发现是版本兼容问题。以下是实测下来最稳的搭配组合:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | SSM老项目的基石,大多数毕设源码基于JDK 8编译 |
| Maven | 3.6.3 | 3.8以上对镜像配置要求更高,没必要冒险 |
| Tomcat | 8.5.x | 兼容javax.servlet规范,Spring 4/5都能跑 |
| MySQL | 5.7 或 8.0 | 5.7最省心,8.0要配对应驱动 |
| mysql-connector-java | 5.1.49 或 8.0.30 | 版本必须和MySQL大版本对应,否则认证报错 |
| IDEA | 2020 之后均可 | 主要是帮你管理Tomcat和Maven |
Tomcat为什么推荐8.5而不是9或10?因为Spring 4.x时代构建的SSM项目,用的还是javax.servlet包名,Tomcat 10把包名改成了jakarta.servlet,老war包部署上去会出现ClassNotFoundException。除非你确定源码里Spring版本是5.x且做了适配,否则不要轻易上Tomcat 10。这个坑用一句话总结就是:Java Web老项目的版本,最怕“太新”。
4.2 修改配置文件:四个必改项
解压项目后,第一步是找到src/main/resources目录下的数据库连接配置文件。文件名可能是db.properties、jdbc.properties或application.properties,内容大同小异。把数据库地址、用户名、密码改成你本机的配置:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/pharmacy?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456连接串里的每个参数都不是装饰。useUnicode=true&characterEncoding=utf8保证中文读写不乱码,少了这两个参数,药品名字段在页面上大概率显示成问号;serverTimezone=Asia/Shanghai解决MySQL驱动在连接时报时区错误的问题;allowPublicKeyRetrieval=true是给MySQL 8用的,不加可能报Public Key Retrieval is not allowed。驱动类名也要注意,MySQL 8对应com.mysql.cj.jdbc.Driver,MySQL 5.7及以下则是com.mysql.jdbc.Driver,写反了启动直接报ClassNotFound。
第二步改Spring的配置装配文件,名字一般是applicationContext.xml或spring-mybatis.xml。重点检查MyBatis的Mapper扫描路径:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.pharmacy.entity"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.pharmacy.dao"/> </bean>mapperLocations指向XML映射文件的位置,常见有两种组织方式:一是放在src/main/resources/mapper下,写成classpath:mapper/*.xml;二是直接放在Java包的dao目录下,这时要写成classpath*:com/pharmacy/dao/*.xml。注意classpath和classpath*的区别,前者只扫第一个匹配路径,后者扫所有Jar包里的类路径,写错位置最常见的报错就是Invalid bound statement (not found)。
4.3 Maven编译、打包、部署三步走
配置改完,先别急着启动Tomcat。在项目根目录执行Maven命令,编译打包一套走完:
# 第一步:清理并打包,跳过单元测试 mvn clean package -DskipTests # 第二步:确认target目录下生成了war包 ls -lh target/*.war # 第三步:把war包复制到Tomcat的webapps目录 cp target/pharmacy.war $CATALINA_HOME/webapps/ # 第四步:启动Tomcat(前台模式,方便看日志) $CATALINA_HOME/bin/catalina.sh run-DskipTests是跳过测试代码,不少毕设项目里测试类依赖了数据库环境,不跳过会导致打包失败。catalina.sh run用前台模式启动,日志直接打在终端上,启动过程如果有异常,你第一时间就能看到,比startup.sh后台启动完再翻日志方便得多。Windows环境下对应的是catalina.bat run。
如果你用的是IDEA,更省事的方式是配置Tomcat Server的Artifact,直接把war包交给IDEA部署,点Debug按钮就能启动。但底层逻辑和命令行没有区别——IDEA只是帮你执行了复制war包和启动Tomcat这两步。
4.4 从登录页到完成一次售药:自测路径
部署成功后访问http://localhost:8080/pharmacy/login,能看到登录页说明项目已经跑起来了。系统管理员的账号密码一般初始化为admin / 123456,脚本里把密码的MD5值写死在了sys_user表中。登录后按这条路径自测一遍,基本能确认全链路可用:
- 打开药品字典,看药品列表能否正常加载,图片和中文是否乱码;
- 新增一个测试药品,填完保存,再刷新列表确认能查到;
- 进采购入库,选刚才那个药品,做一个入库单,库存数量增加;
- 到收银台模拟售药,选择药品、填写数量,确认库存扣减生效;
- 回到库存列表,看库存是否比售药前减少,操作日志有没有记录。
这五步走完,说明从SpringMVC到MyBatis再到MySQL的主链路全部通了。如果哪一步卡住,回到日志看异常堆栈,绝大多数问题都出在数据库连接和MyBatis映射这两个环节,这正是下一章要重点梳理的。
5. 常见问题与排查:SSM药房系统跑不起来的5个坑
5.1 启动报Communications link failure:MySQL连接没谈拢
现象:Tomcat启动日志里出现Communications link failure或Connection refused,应用直接启动失败。
原因:最多的情况是MySQL服务没启动,其次是密码不对,最后是连接串里的端口写错。SSM项目启动时Spring会初始化数据源并试着获取连接,连接失败就会把这个异常抛出来。
解决:先用命令行确认MySQL本身能连:mysql -u root -p能正常进入,说明服务没问题,再看db.properties里的密码、端口是否匹配。还有一个隐藏问题,MySQL 8的默认认证插件是caching_sha2_password,如果驱动还停留在5.1.x,也会报认证错误,换8.0.x的驱动并按上一章配置allowPublicKeyRetrieval=true即可。
5.2 404欢迎页打不开:上下文路径和部署目录对不上
现象:Tomcat启动正常,但访问http://localhost:8080/是404,访问http://localhost:8080/pharmacy/login也一样。
原因:一种是war包部署后没有生成对应的解压目录;另一种是浏览器访问的URL少了项目上下文路径。Tomcat默认把war包解压到webapps下同名的目录,war包叫pharmacy.war,上下文路径就是/pharmacy,不带这个前缀必然404。
解决:先看webapps目录下有没有pharmacy这个文件夹,没有说明war包没部署成功。再看启动日志里有没有Deploying web application archive和has finished字样。浏览器访问时带上上下文路径:http://localhost:8080/pharmacy/,很多项目的欢迎页就挂在/login或/index上。
5.3 Invalid bound statement:MyBatis映射文件没被扫到
现象:页面操作触发查询时报Invalid bound statement (not found): com.pharmacy.dao.DrugDao.selectList,而数据库连接完全正常。
原因:Mapper接口的Java方法没有在XML映射文件里找到对应的<select>或<insert>。绝大多数情况是mapperLocations配置的路径和XML文件实际位置不匹配,编译后的XML没有进target/classes,所以运行时扫不到。
解决:先检查编译输出目录,比如target/classes/mapper下有没有对应的XML文件;没有的话说明Maven没有把XML当资源编译进去,在pom.xml的<build><resources>里加上src/main/java下的XML包含规则。再看XML文件的namespace是否指向了正确的Mapper接口,方法id和接口方法名是否一致。这三处对齐,问题就解决了。
5.4 中文乱码:数据库、JSP、Tomcat三处要统一UTF-8
现象:登录后药品名称显示成???,或者页面上的中文全是乱码。
原因:乱码问题很少是单点原因,通常是传递链路里某一环字符集不一致。数据库建表不是utf8mb4、JDBC连接串没加characterEncoding=utf8、JSP页面没加pageEncoding="UTF-8"、Tomcat对URL参数没设URIEncoding,这四处只要有一处不一致,中文就会在某个环节变形。
解决:先改数据库连接串,按第4章的配置补全参数;再检查JSP页面头部是否声明了pageEncoding="UTF-8";最后在Tomcat的server.xml里给<Connector>加上URIEncoding="UTF-8"。三处统一后重建数据,乱码基本能解决。注意已经乱码进库的数据需要清掉重导,改配置不会自动修复旧数据。
5.5 MySQL 8连不上的玄学:驱动版本和认证插件
现象:MySQL客户端能正常连接,驱动也换成了com.mysql.cj.jdbc.Driver,但项目启动还是报Unable to load authentication plugin 'caching_sha2_password'。
原因:MySQL 8默认创建用户用的是caching_sha2_password认证插件,老版驱动不认识这个插件。这个问题在毕设环境里很常见,因为很多人本机装的就是MySQL 8,而项目自带的驱动是5.1.x。
解决:把pom.xml里的mysql-connector-java版本升到8.0.x,同时驱动类名改成com.mysql.cj.jdbc.Driver,连接串加上allowPublicKeyRetrieval=true&useSSL=false。如果你不想动版本,也可以登录MySQL执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';,把用户认证插件改回老协议。两条路任选其一,项目就能连上了。
6. 从能跑到高分:答辩前的优化与验证思路
6.1 扣库存的事务与乐观更新:并发卖同一盒药怎么防超卖
药房系统看起来简单,评委最爱追问的一个问题是“两个人同时买最后一盒药怎么办”。很多同学写销售逻辑时先SELECT stock_qty,判断库存大于0,再UPDATE减库存,这个流程在并发下会出问题——两次查询都发现库存为1,然后各自扣减,库存变成负数。解决思路是用一条带条件的UPDATE,让数据库来保证原子性:
@Transactional(rollbackFor = Exception.class) public void createSaleOrder(SaleOrder order, List<SaleItem> items) { for (SaleItem item : items) { int rows = stockMapper.deductStock(item.getDrugId(), item.getQuantity()); if (rows == 0) { throw new RuntimeException("库存不足,销售单已回滚"); } } saleOrderMapper.insert(order); saleOrderMapper.insertItems(items); }UPDATE drug_stock SET stock_qty = stock_qty - #{qty} WHERE drug_id = #{drugId} AND stock_qty >= #{qty}deductStock返回的条数只有两种情况:更新成功返回1,库存不足或记录不存在返回0,利用数据库行锁天然解决了并发扣减问题。@Transactional保证销售单和库存扣减在同一个事务里,任何一个明细失败,整个销售单撤销,不会出现卖了药库存没减的情况。这一小段代码能讲出事务、并发、SQL优化三个层面的深度,是答辩的实打实得分点。
6.2 列表页提速三件事:分页、缓存与懒加载
药品一多,列表页会明显变慢。最基础的手段是分页,用PageHelper插件,一行PageHelper.startPage(pageNum, pageSize);接在查询方法前,MyBatis会自动拼接LIMIT语句,页面上同时拿到总数和当前页数据,这是列表页优化的第一档。再往上可以做热点数据缓存,把药品字典这类改动少、读得勤的数据放进缓存,用Redis还是本地缓存看环境条件,能够在答辩时说清楚缓存更新策略就够了——更新药品时同步清掉对应缓存,防止脏读。图片、大字段用懒加载,列表页先不查明细,点开详情再加载,这三个点足够撑起“系统优化”的章节。
6.3 演示脚本:五分钟把业务闭环讲给评委听
答辩演示最忌只展示CRUD。一条完整的业务闭环比十个独立页面都管用,按这个顺序走:
| 步骤 | 操作 | 让评委看什么 |
|---|---|---|
| 1 | 管理员登录 | 登录校验和权限跳转 |
| 2 | 新建一个测试药品 | 药品档案字段校验 |
| 3 | 采购入库该药品 | 库存增加、流水记录产生 |
| 4 | 收银台销售该药品 | 库存扣减、销售单生成 |
| 5 | 回到库存列表 | 库存减少、预警标红的触发逻辑 |
每一步之间都要把因果关系讲出来:采购导致库存增加,销售导致库存减少,库存变化产生流水,流水是后续报表的数据来源。这套逻辑讲通,评委看到的不只是代码,而是你对业务的理解。
我写代码有个习惯,项目跑通交付之前,一定把数据库重新导入一遍,密码初始化成文档里的默认值,确保任何一台机器按着操作说明都能从零跑起来。当年有同学答辩时用的还是指导老师机器上的旧数据,演示完老师一问“本地怎么部署”,当场卡壳。跑通只是起点,别人能复现才是交付标准。希望帮到你。
本文还有配套的精品资源,点击获取