SSM药房管理系统实战:从框架原理到库存扣减事务解析

发布时间:2026/10/26 15:50:11
SSM药房管理系统实战:从框架原理到库存扣减事务解析 简介一套基于SSM框架的药房管理系统完整毕业设计项目面向计算机相关专业毕业生及需要项目实战的Java学习者。系统涵盖药品信息管理、库存、采购、销售等典型药房业务模块可帮助读者掌握Spring、SpringMVC、MyBatis三层架构的整合与开发流程。资源包共1222个文件约19.56MB包含48个Java源文件、70个JSP页面、18个XML配置、SQL数据库脚本以及CSS/JS/HTML等前端资源其中JSP视图与前端资源用于界面展示Java源文件和XML配置体现业务逻辑与框架整合目录结构清晰便于定位学习。源码经本地编译调试评审得分98分可直接运行或作为二次开发基础。目前已有174人学习下载适合用于毕业设计参考、课程设计或SSM框架项目练习。1. 从毕设选题到可运行系统药房管理系统到底解决了什么每年到这个节点总有一批人被同一个场景卡住从网上下载了基于SSM框架的药房管理系统源码数据库脚本也导进去了Tomcat也点启动页面却停在登录框转圈或者一查药品就报500。更尴尬的是答辩现场老师问一句“你的库存扣减怎么保证不超卖”答不上来分数直接从“高分毕设”掉到“及格边缘”。我见过太多人把精力全耗在配环境上配完能跑就以为完事其实对SSM三件套怎么协作、业务表和接口怎么衔接还是一团黑。这个药房管理系统本质上是一个典型的Java Web进销存项目药品信息管理、入库出库、销售收银、供应商维护、库存预警再加一个简单的权限角色区分。技术栈锁死在Spring SpringMVC MyBatis配MySQL数据库脚本。它适合两类人一类是正在做Java毕设、需要一个能讲清楚又能落地的项目兜底的学生另一类是刚学完SSM框架、想看看三个框架怎么集成到同一个工程里的初学者。先说结论源码能跑通只是起点真正决定能不能拿高分的是你对请求链路、事务边界和库存扣减这三件事的理解。下面我把整个落地路径拆开讲。2. SSM三件套在药房系统里各自管哪块选型理由与核心流程很多人把SSM当成一个整体在配出了问题不知道找谁。其实三个框架在药房系统里分工非常明确Spring管对象和事务SpringMVC管HTTP请求的路由和参数绑定MyBatis管SQL和结果映射。它们各自只管自己那一层协作起来才是一条完整的调用链。这一章先把分工讲清楚后面配置和排错才有依据。2.1 Spring的IoC与事务在药房订单里的位置药房系统的核心业务是销售和入库。一次销售动作至少涉及两件事往销售订单表插一条记录再把药品库存表的库存数量减掉。如果没有事务第二步失败时第一步已经提交账就对不上了。Spring在这里做的事就是用Transactional把这两步包进同一个数据库事务里要么全成要么全败。Spring的IoC容器在这个项目里体现在Service层。常见的做法是Controller里不直接new一个Service对象而是通过注解注入。比如Autowired一个DrugService容器启动时帮你把实现类实例化好并注入进去。这对毕设答辩是一个很值得讲的点你的各个业务模块之间是解耦的替换实现类不需要动调用方代码。配置上通常在spring-mybatis.xml里用bean或注解扫描包路径来声明这些对象context:component-scan base-packagecom.xxx.service/这种写法就是Spring扫描Service层注解的入口。事务配置要单独说一句。很多人Transactional注解加了但库存还是能扣成负数问题往往出在事务管理器没有接到数据源上。DataSourceTransactionManager必须显式把dataSource属性指过去不然Spring管理的事务和你连接池里的连接不是同一个会话事务注解形同虚设。这个我在第5章踩坑部分还会再展开。2.2 SpringMVC处理一次药品查询请求的完整路径先走一遍请求路径后面排查404、500时会非常有用。用户在浏览器输入/drug/list?keyword阿莫西林请求先到web.xml里配置的DispatcherServlet它的url-pattern通常设为/所有请求都会进来。接着SpringMVC根据RequestMapping(/drug/list)找到对应的Controller方法把keyword参数绑定到方法入参上。Controller拿到参数后自己不写SQL而是调用Service接口Service再调Mapper接口Mapper的XML里写真正的SQL语句。返回结果从后端一层层往前传Controller把查到的药品列表放进ModelAndView或Model里再交给视图解析器渲染JSP页面。整个过程里SpringMVC帮你做了参数转换、路径匹配和视图渲染三件事你只需要关注Controller里那几行代码。有个细节容易忽略SpringMVC默认不处理静态资源请求。如果JSP页面里引用了CSS、JS文件而你没有在spring-mvc.xml里配置mvc:resources mapping/static/** location/static//这些资源会被DispatcherServlet拦截后报404。很多页面长得丑、样式加载不出来就是这个配置漏了和第5章我讲的静态资源拦截是同一个坑。2.3 MyBatis的Mapper层库存扣减为什么不能只写一条update在SSM项目里MyBatis负责把Java方法和SQL映射起来。药房系统里最容易被答辩老师追问的就是库存扣减。初学者常写的SQL是UPDATE drug_stock SET stock stock - 1 WHERE drug_id ?。表面看没问题但你要考虑并发场景两个人同时买同一个药两个请求同时读到库存是10都减1最后库存变成9而不是8。这在数据库层面叫丢失更新。要修正它常见做法是把扣减条件加上库存数量的判断UPDATE drug_stock SET stock stock - #{num} WHERE drug_id #{id} AND stock #{num}。这句话的意思是先检查库存够不够够了才扣不够就不动这条记录同时配合事务回滚保证不会出现负数库存。MyBatis的Mapper XML里写动态SQL也很方便比如if testkeyword ! null and keyword ! 来拼查询条件比JDBC手动拼接字符串安全得多也基本杜绝了SQL注入。MyBatis另一个值得讲清楚的是结果映射。表字段是drug_idJava属性是drugId如果MyBatis没开驼峰映射配置查出来的对象里drugId就是null。解决方式是在mybatis-config.xml里加setting namemapUnderscoreToCamelCase valuetrue/。这一句话能省掉大半的resultMap手写工作量是我每接到一个SSM项目第一个检查的配置项。3. 把数据库脚本跑起来建库建表与初始化数据拿到源码包第一步通常不是看代码而是把数据库脚本导入MySQL。这一步翻车率极高十个人里有三四个卡在这里。原因集中在导入顺序、字符集和MySQL 8的认证方式上。这一章按照我平时给项目初始化数据库的顺序来讲你可以直接照着做。3.1 建库与导入脚本命令行与Navicat两条路线先说命令行方式。假设脚本文件叫pharmacy.sql在Windows的cmd里执行注意先确认MySQL的bin目录在PATH里mysql -uroot -p123456 -e CREATE DATABASE IF NOT EXISTS pharmacy DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -uroot -p123456 --default-character-setutf8mb4 pharmacy D:/sql/pharmacy.sql第一条命令的作用是创建数据库utf8mb4比utf8多支持四字节字符比如有些生僻字和emoji药房系统里药品名称和商品名经常带特殊字符用utf8mb4能少很多幺蛾子。第二条命令才是把脚本导入--default-character-setutf8mb4这个参数非常关键不加的话脚本里的中文注释和中文数据容易变成乱码。用图形工具也完全可以Navicat或DataGrip里右键连接新建数据库字符集选utf8mb4然后选中库右键“运行SQL文件”选择脚本路径执行即可。注意执行完一定要刷新一下库列表有些版本不刷新不显示新表。导入完先看左边表列表的数量如果只有几十张里的两三张说明脚本中间某个语句报错中断了得点开消息面板看具体是哪个表的建表语句挂了。3.2 三张核心表的关系药品、库存、销售记录药房系统的表通常不止三张但理解了核心三张整个系统就懂了一半。我拿最常见的表设计来说明表名核心字段职责drug_infodrug_id,drug_code,drug_name,specification,unit,price,prescription_type,expire_date药品静态信息价格、规格、是否处方药drug_stockstock_id,drug_id,total_stock,warning_line,update_time当前库存余量和药品一对一的对应关系sale_recordrecord_id,drug_id,sale_num,sale_price,sale_time,operator_id每笔销售流水记录卖了多少、多少钱、谁操作药品表和库存表之间通过drug_id关联销售记录表和药品表之间也是drug_id。这和你正常去药店买药是一一对应的每个药品有一条库存数每次卖出加一条销售流水同时库存减一。这里要注意sale_record应该保存卖出的单价快照而不是实时去查drug_info里的当前价格。因为药价会调你回头对账时必须用卖出去那一刻的价格这就是数据库设计里说的冗余存储是为了历史一致性。建表脚本样例大致长这样简化版实际脚本会更全CREATE TABLE drug_info ( drug_id int(11) NOT NULL AUTO_INCREMENT, drug_code varchar(32) NOT NULL COMMENT 药品编码, drug_name varchar(128) NOT NULL COMMENT 药品名称, specification varchar(64) DEFAULT NULL COMMENT 规格, price decimal(10,2) NOT NULL COMMENT 零售价, prescription_type tinyint(1) DEFAULT 0 COMMENT 0非处方 1处方, expire_date date DEFAULT NULL COMMENT 有效期, PRIMARY KEY (drug_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;注意ENGINEInnoDB必须指定MyISAM不支持事务药房系统的销售和库存扣减一旦跨表非事务引擎就废了这个坑我在第5章详细说。decimal(10,2)是货币标准写法别用float浮点算钱会丢精度对账时候差几分钱够你找半天的。3.3 初始化账号与数据管理员、收银员角色怎么分脚本里一般会自带几条初始用户数据。最常见的角色设计是管理员和营业员收银员。管理员可以操作药品增删改、供应商管理、查看所有销售记录营业员只能做销售开单、查询药品库存。这个权限差別通常用一张sys_user表加一个user_role字段来实现或者独立一张角色表。字段设计一般是CREATE TABLE sys_user ( user_id int(11) NOT NULL AUTO_INCREMENT, username varchar(32) NOT NULL, password varchar(64) NOT NULL COMMENT MD5或BCrypt加密, real_name varchar(32) DEFAULT NULL, role_id int(11) NOT NULL COMMENT 1管理员 2营业员, status tinyint(1) DEFAULT 1, PRIMARY KEY (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;导入脚本后一定要先查一下初始用户名和密码很多项目的初始密码是123456但存储方式可能是MD5加密后的串。登录不上不要急着怀疑代码先用SQL查一下SELECT * FROM sys_user;你看到的密码字段如果是e10adc3949ba59abbe56e057f20f883e这串那就是123456的MD5值。如果前端登录用的不是MD5而是BCrypt你也可以在登录页试一下明文123456能进就说明过滤器里做了加密比较。这块搞清楚面试和答辩时讲到“用户认证怎么做的”就有话说了。4. 本地跑通源码从JDK到Tomcat的完启动流程数据库就绪后下一步是在IDEA里把工程跑起来。SSM项目跑不起来的原因高度集中在版本不匹配和配置数据源上。这一章的环境版本搭配是这么多年下来最不容易出事的组合你按这个来基本能少走一半弯路。4.1 环境版本对照JDK 8、Tomcat 8.5、Maven 3.6的搭配理由SSM的大多数毕设项目都是在JDK 8时代写的用JDK 17去跑大概率编译不过因为javax.servlet从JDK 9开始就移除了老项目依赖的是javax命名空间新环境里根本找不到这些类。我见过太多人卡在这一步其实不是项目烂是版本错配。稳妥的组合是组件推荐版本原因JDK1.88u202以上大多数SSM老代码只兼容到javax命名空间Tomcat8.5.x匹配Servlet 3.1规范内置运行方式友好Maven3.6.x和IDEA自带Maven兼容度最高依赖解析稳定MySQL5.7 或 8.05.7最稳8.0需注意驱动和时区配置IDEA2020以后的版本对Maven和Tomcat插件支持都比较成熟Maven 3.6的settings.xml里最好配一下阿里云镜像不然SSM依赖里那些老包在中央仓库拉取很慢还容易超时。镜像配置是mirror节点里把central代理到https://maven.aliyun.com/repository/public这个不属于外链是常规操作。4.2 IDEA导入与配置文件改写IDEA导入SSM工程第一步是File - Open选择pom.xml所在目录IDEA会识别为Maven项目并开始解析依赖。解析过程中右下角会转圈第一次可能要下载几百MB依赖别急着操作。导入完成后重点检查三处配置。第一处jdbc.properties——这是连不上数据库的第一大原因。常见写法是jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/pharmacy?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456这里有三点要强调。driver如果是MySQL 8.0要用com.mysql.cj.jdbc.Driver用老的com.mysql.jdbc.Driver会报警告但不一定报错。useSSLfalse必须加MySQL 8默认启用SSL校验不加会报Communications link failure。serverTimezoneAsia/Shanghai必须加否则MySQL 8会报The server time zone value的异常。因为中国时区问题很多人卡在这三个参数上每一个都是血泪经验。第二处spring-mybatis.xml里的Mapper扫描路径。检查mapper-locations配置的classpath:mapper/*.xml和你实际的XML存放路径是否一致。这个对不上就报Invalid bound statement (not found)Service调用Mapper方法时必炸。第三处pom.xml的依赖坐标。常见需要的是spring-webmvc、mybatis、mybatis-spring、mysql-connector-java版本号5.1.49或8.x对应上数据库版本、druid连接池、jackson-databind前后端JSON交互、jstl和javax.servlet-apiprovided作用域给Tomcat用。缺了jackson前端Ajax请求返回的JSON会变成一串乱码或直接报406。4.3 启动后验证登录、查药品、做一次入库操作的请求链路配置改完就点Tomcat启动。启动日志看到Server startup in [xxx] milliseconds字样后打开浏览器访问http://localhost:8080/项目名/。这一步经常有人卡住因为SSM项目打的包通常是war项目名要和pom.xml里的finalName保持一致。访问不放心就看一下IDEA下方的Tomcat Console如果出现Artifact xxx:war exploded: Error during artifact deployment说明你的war包没打对或Web模块没配置好检查Project Structure - Artifacts里的Output Layout是否包含lib下的所有依赖。验证功能不要只登录一下就完事按业务流程走一遍才能确认框架集成没断。先登录走一次SpringMVC的登录验证再点开药品列表走一次Mapper查询然后新建一条入库单走一次Service事务提交。其中入库操作是测试事务最方便的手段——在入库ServiceImpl的Transactional方法里加个System.out.println(1/0);如果提交不了且数据库里没有新增记录说明事务生效。测试完记得把这行删掉。5. 毕设答辩前必查的坑导入失败、中文乱码、功能断层这一章是排错清单每一条都是实战里反复出现的。按照现象→原因→解决的顺序写你对照着自己项目的现象找省得从报错日志里瞎猜。5.1 改完jdbc配置还是连不上数据库时区、SSL与驱动三连问现象数据库明明开着账号密码都对启动Tomcat时就是报Cannot create PoolableConnectionFactory或Communications link failure。原因排在前三的是MySQL 8的驱动类写成了老的com.mysql.jdbc.Driver连接串没有serverTimezone或者是useSSL没设置为false。解决的常规顺序是先确认MySQL版本5.7用老驱动8.0用新驱动再检查jdbc:mysql://localhost:3306/pharmacy?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai这三个参数缺一不可最后重启Tomcat。还有一种情况是3306端口被占用netstat -ano | findstr 3306看下端口是否被别的进程占着。5.2 药品名称中文全变问号characterEncoding与表collation双管齐下现象从界面录入的“阿莫西林胶囊”存到库里变成“”查出来也是问号。原因有两个层面连接串没指定characterEncodingutf8时JDBC会按服务器默认编码可能是latin1传参另一个是建库时字符集写成了utf8而不是utf8mb4或者表的排序规则被改成了latin1_swedish_ci。解决方式先确认CREATE DATABASE时用的字符集然后改连接串为characterEncodingutf8如果数据已经乱了就在数据库里执行ALTER TABLE drug_info CONVERT TO CHARACTER SET utf8mb4;。改完重启Tomcat重新录入一条带中文的数据验证。5.3 库存能扣成负数事务不生效与并发不加锁现象销售开单时故意卖超库存变成负的数据库居然也接受了。原因有两个第一个是事务管理器没生效。你去检查spring-mybatis.xml如果bean idtransactionManager里没有引用dataSource那个bean或者tx:annotation-driven transaction-managertransactionManager/这行注释掉了那Transactional完全不工作异常不回滚。第二个是扣减SQL本身没加库存判断就是我在第2.3节说的AND stock #{num}那个条件。解决方式先给Service实现类的public方法加上Transactional(rollbackFor Exception.class)再把SQL改成带条件的更新语句。这里我用的习惯是加rollbackFor Exception.class因为Spring默认只对RuntimeException回滚检查异常比如自定义的StockNotEnoughException不会自动回滚。5.4 页面报404或500但控制台没有堆栈静态资源拦截与Mapper扫描遗漏现象登录页跳转后CSS全丢页面裸奔或者点按钮弹窗里显示500但后台catalina.out日志干干净净。前者是SpringMVC的静态资源拦截解决方式很简单在spring-mvc.xml里配置mvc:resources mapping/static/** location/static// mvc:annotation-driven/后者通常分两种一是jackson库没引入Spring无法把返回的对象转成JSON报500但日志被前端吞了二是MyBatis的mapper-locations配置路径不对导致SQL没绑定报Invalid bound statement (not found)。解决方式打开浏览器F12切到Network标签看实际请求的状态码和响应信息比看后台日志更快找到问题。6. 从“能跑”到“高分”给药房系统加一个让答辩加分的小功能如果你的系统已经跑通登录、库存、销售这几条主链路都没问题接下来加一个独立小功能能让答辩老师对你另眼相看。我推荐做“药品有效期预警”它既有业务价值又不需要动系统主结构2小时能搞定但能讲的知识点非常多——定时任务、SQL条件和数据可视化都沾得上。6.1 用Spring定时任务加一个过期预警在Service里加一个方法用Spring自带的Scheduled注解定时扫描把距有效期不足3个月的药品查出来。首先确认Spring配置文件里开启了定时任务task:annotation-driven/然后在Service实现类里写Scheduled(cron 0 0 9 * * ?) // 每天早上9点执行 public void checkExpireDrugs() { LocalDate warningDate LocalDate.now().plusMonths(3); ListDrugInfo expireList drugMapper.selectExpireDrugs(java.sql.Date.valueOf(warningDate)); for (DrugInfo drug : expireList) { log.warn(药品 {} 将于 {} 到期请及时处理, drug.getDrugName(), drug.getExpireDate()); } // 还可以把结果写入一张预警表供前端查询展示 }这里Scheduled是Spring的定时任务注解cron表达式0 0 9 * * ?表示每天9点触发。对应Mapper里就一句SQLSELECT * FROM drug_info WHERE expire_date IS NOT NULL AND expire_date #{warningDate}。这个功能往前端一接首页放个“近效期药品提醒”的表格屏幕上瞬间多了一个能演示的模块。答辩时候老师问“这个预警怎么实现的”你就可以讲清楚定时任务、时间计算和SQL筛选三个点比背概念强得多。6.2 验证预警功能的两条路径第一条路径改系统时间或数据库数据来强制触发。把某条药品记录的expire_date改成当前日期加60天手动调用一次Service方法看日志是否输出预警信息。第二条路径用JUnit写一个简单的测试方法直接调checkExpireDrugs()打印返回值。我一般在写这类小功能后都会跑一条测试路径确认不是为了覆盖率是为了答辩时能回答“你有没有测过”这个问题——用事实说话比“应该没问题”有说服力太多。我的习惯是给客户或自己做验收时总会随手在系统里做一次破坏性操作比如把库存改成1再卖2件、把药品有效期改成昨天再跑一次预警任务。因为“能跑通的系统”很多“怎么测都不会出问题”的系统才是真正值的。希望这条经验对你也有用希望帮到你把项目从“能跑”变成“能讲清楚、能扛住追问”。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询