Java SSM+Vue药房管理系统:从环境搭建到库存预警实战解析
简介药房药品管理系统源码采用SSMVue技术栈面向需要完成Java课程设计、毕业设计、项目实训或SSM/Vue实战练习的开发者。后端基于SSM框架整合Spring、SpringMVC与MyBatis并利用SpringBoot简化配置通过分层解耦实现业务逻辑与视图分离前端使用Vue.js构建交互界面数据存储采用MySQL 5.7及以上版本配合Maven管理依赖并可使用Navicat/SQLyog完成数据库维护。压缩包大小约22.7MB文件总数未单独列出源码文件按后端、前端等模块组织文件明细暂未公开但完整度足以支撑本地运行与二次开发这套结构同样适合作为中小型管理系统开发的参考模板。目前已有83人学习适合用它快速理解药房药品库存、销售管理等业务场景下的完整实现思路资源主体包含可直接导入IDEA/Eclipse的工程源码、数据库相关配置与前端页面代码可帮助读者搭建环境后掌握SSMVue项目从后端接口到前端展示的整合流程。1. Java SSM Vue 药房药品管理系统先搞清楚这份源码能解决什么做药店管理系统这类业务项目很多人上来就搜 springbootvue 的通用模板但真正落地时才发现药品的批次、效期、库存上下限、供应商往来这些都是普通 CRUD 模板覆盖不到的业务细节。这份 java-ssmvue 药房药品管理系统源码解决的就是这类中小型药房的信息化管理问题——药品入库、出库、库存查询、预警提示、基础信息维护前后端分离后端用 SSMSpring SpringMVC MyBatis整合 SpringBoot前端用 Vue数据库用 MySQL 5.7Maven 统一管依赖。适合做 Java 课程设计、毕业设计或者想完整走一遍前后端分离开发流程的开发者。它不是教科书项目是一个能跑起来、能二次开发的真实业务系统下面我按拆过的项目经验把它从结构到踩坑讲透。2. SSM 与 SpringBoot 的结合这份源码的后端组织逻辑2.1 为什么是 SSM 而不是单纯 SpringBoot很多人看到项目里同时出现 SpringBoot 和 SSM 会有点懵觉得 SpringBoot 已经把配置简化了还要 SSM 干什么。实际上这份源码的组织方式是用 SpringBoot 作为应用启动基础内部按 SSM 的三层结构组织业务代码——Spring 管 Bean 和事务SpringMVC 管请求路由MyBatis 管数据库映射。这种组合在课程设计和中小型项目中很常见因为 SSM 分层清晰每一层职责明确调试的时候能快速定位是 Controller 的问题还是 Mapper 的问题。从代码目录上你能看到典型的包结构src/main/java ├── com.pharmacy.controller // SpringMVC 控制层 ├── com.pharmacy.service // 业务逻辑层 ├── com.pharmacy.mapper // MyBatis 数据访问层 ├── com.pharmacy.entity // 实体类 ├── com.pharmacy.config // SpringBoot 配置类 └── PharmacyApplication.java // 启动类Controller 层负责接收前端请求不会直接操作数据库Service 层写业务规则比如库存扣减时要同时更新库存表和出库记录Mapper 层只负责 SQL 交互。这种分层的好处是如果前端 Vue 传参格式变了只改 Controller 的接收参数业务逻辑不用动如果数据库表结构调整只改 Mapper 层接口对外保持不变。2.2 SpringMVC 的请求流转与参数绑定SpringMVC 在这套系统里承担的是「前端请求进来该怎么走」的职责。以药品列表查询为例前端 Vue 发送一个 GET 请求到/drug/listSpringMVC 根据RequestMapping找到对应的 Controller 方法然后把请求参数绑定到方法的入参上。一个典型的药品分页查询 ControllerRestController RequestMapping(/drug) public class DrugController { Autowired private DrugService drugService; GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer limit, String keyword) { PageInfoDrug pageInfo drugService.queryPage(page, limit, keyword); return Result.success(pageInfo); } }逻辑说明RestController表示这个类所有方法返回 JSON 数据不需要再写ResponseBody。RequestParam接收前端传来的 page 和 limit 参数defaultValue保证前端没传时不会报空指针。keyword 是可选参数用于药品名称模糊查询。这套写法你在网上看到的 SSM 项目基本一致唯一区别是 SpringBoot 环境下不需要配置web.xml和 dispatcherServlet启动类一跑整个 MVC 环境就自动装配好了这对新手来说少踩很多配置文件的地雷。2.3 MyBatis 的 Mapper 层SQL 写在 XML 里还是注解里MyBatis 是这套源码里最值得看的部分。药房药品管理系统涉及的查询条件很多按药品名称查、按生产批次查、按有效期查、按库存状态查。常见做法是把 SQL 写在 XML 文件里因为动态 SQL 在 XML 里写if、where标签比较灵活改 SQL 不用重新编译。举个例子药品条件查询的 Mapper XMLselect idqueryDrugList resultTypecom.pharmacy.entity.Drug SELECT * FROM drug where if testkeyword ! null and keyword ! AND drug_name LIKE CONCAT(%, #{keyword}, %) /if if teststockStatus ! null AND stock_status #{stockStatus} /if /where ORDER BY update_time DESC /select参数说明where标签会自动处理第一个AND前的多余连接符不用手动写WHERE 11。#{keyword}是预编译参数占位符能防止 SQL 注入比直接拼接字符串安全得多。stockStatus用于按库存状态筛选比如 0 表示正常1 表示预警2 表示缺货。很多人在 SSM 项目里会纠结 SQL 写在注解还是 XML我的建议是单表简单 CRUD 用注解Select、Insert就行多表关联和动态条件查询一律放 XML。这份源码里药品入库记录、出库记录都涉及联表查询XML 文件维护起来比注释里的字符串舒服得多。3. 环境搭建与数据库初始化把源码跑起来的具体路径3.1 IDEA 导入项目与 Maven 依赖下载拿到源码第一步不是急着改代码而是先在本地把环境跑通。打开 IDEA选择File - Open选中项目的pom.xml或根目录IDEA 会自动识别为 Maven 项目并开始下载依赖。pom.xml 里的关键依赖parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.3.4.RELEASE/version /parent dependencies dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.1.4/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.47/version /dependency /dependencies参数说明spring-boot-starter-parent是 SpringBoot 的依赖管理父工程它帮我们锁定了 Spring 和 SpringBoot 各模块的版本子项目里不需要再一个个写版本号。mybatis-spring-boot-starter是 MyBatis 官方提供的 SpringBoot 整合包会自动配置 SqlSessionFactory。mysql 驱动要注意版本MySQL 5.7 用 5.1.47 肯定没问题如果你本地是 MySQL 8.x要把驱动换成com.mysql.cj.jdbc.Driver对应的 8.x 版本。Maven 下载依赖慢是国内永恒的话题常见做法是给 IDEA 配置阿里云镜像。修改settings.xmlmirror idaliyunmaven/id mirrorOf*/mirrorOf namealiyun maven/name urlhttps://maven.aliyun.com/repository/public/url /mirror配置后重启 IDEA依赖下载速度能快一个量级。这一步不做后面每次编译都要干等。3.2 MySQL 数据库创建与 SQL 脚本导入数据库工具推荐用 Navicat因为它对中文表名、字段名的兼容性处理得比较好导入导出不容易乱码。打开 Navicat新建连接连接名随意主机 localhost端口 3306用户名 root密码填你自己 MySQL 的密码新建数据库数据库名建议与项目配置文件保持一致编码选utf8mb4。然后用 Navicat 运行 SQL 脚本数据库初始化完成后你会看到类似这样的一组表表名作用关键字段sys_user系统用户表user_id, username, password, roledrug_info药品基础信息表drug_id, drug_name, specification, manufacturerdrug_stock药品库存表drug_id, stock_quantity, warning_line, expired_datesupplier_info供应商表supplier_id, name, contact_phoneinbound_record入库记录表record_id, drug_id, quantity, inbound_timeoutbound_record出库记录表record_id, drug_id, quantity, outbound_time这套表结构覆盖了药房管理最核心的闭环药品档案、供应商往来、入库出库流水、库存实时状态。注意drug_stock表里通常有warning_line字段这是库存预警的基础——当stock_quantity小于warning_line时前端就该显示预警状态。3.3 application.yml 配置与前端项目启动数据库导好后修改后端的application.yml把账号密码改成你自己的server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/pharmacy_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.pharmacy.entity参数说明serverTimezoneAsia/Shanghai是很多项目跑起来报错的关键点MySQL 驱动 5.1.47 以后如果不配时区会提示 CST 时区无法识别。mapper-locations指向 Mapper XML 文件所在目录MyBatis 启动时会自动扫描加载。type-aliases-package让 XML 里的resultType可以直接写Drug而不用写全限定类名。前端部分进入frontend目录npm install npm run devnpm install安装 Vue 项目依赖可能需要几分钟如果报权限错误就用sudo。npm run dev启动开发服务器默认端口一般是 8080 已经被后端占了所以前端默认端口通常是 8081或者你可以在vue.config.js里改module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }proxy 配置是为了解决开发环境的跨域问题——前端请求/api/loginVue 开发服务器会自动转发到http://localhost:8080/api/login后端的 SpringMVC 收到请求后正常处理。这个转发是前端代理层面做的后端不需要额外配 CORS。4. 核心业务流程走通从登录鉴权到库存预警的实现4.1 登录鉴权与 Session 管理药房管理系统肯定要区分管理员和普通操作员。后端的登录接口通常长这样PostMapping(/login) public Result login(RequestBody SysUser user, HttpSession session) { SysUser dbUser userService.findByUsername(user.getUsername()); if (dbUser ! null dbUser.getPassword().equals(MD5Util.md5(user.getPassword()))) { session.setAttribute(userId, dbUser.getUserId()); session.setAttribute(role, dbUser.getRole()); return Result.success(登录成功); } return Result.error(用户名或密码错误); }逻辑说明前端 Vue 用 axios 发送 POST 请求body 里是 JSON 格式的用户名密码。后端查到数据库里的用户后用 MD5 加密后的密码比对密码不建议明文存储。比对成功就把用户信息放进 Session后续请求通过拦截器判断 Session 里有没有 userId。对应的拦截器注册方式Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /register, /static/**); } }参数说明addPathPatterns(/**)表示拦截所有接口excludePathPatterns放行登录和注册。如果你在 IDEA 里启动后访问接口一直 401 或重定向到登录页先看这里路径写的对不对。4.2 药品管理模块一次完整的 CRUD 请求链路前端药品列表页打开时会自动调用后端接口加载数据。以「新增药品」为例从点击提交按钮到数据库写入成功整条链路是前端 Vue 组件里的提交方法submitForm() { axios.post(/api/drug/add, this.drugForm) .then(res { if (res.data.code 200) { this.$message.success(添加成功); this.getDrugList(); } else { this.$message.error(res.data.msg); } }) .catch(err { this.$message.error(请求失败请检查后端是否启动); }); }这段代码的逻辑是把表单里的药品数据通过 axios 的 POST 方法发给后端/api/drug/add接口开发环境会经过 8081 端口的 proxy 转发到后端 8080。后端 Controller 接收后调用 Service 层Service 层检查药品名称是否已存在不存在则调用 Mapper 层执行 INSERT。后端新增药品的 Service 实现Service public class DrugServiceImpl implements DrugService { Autowired private DrugMapper drugMapper; Override Transactional public void addDrug(Drug drug) { int exists drugMapper.checkNameExist(drug.getDrugName()); if (exists 0) { throw new RuntimeException(药品名称已存在); } drug.setCreateTime(new Date()); drug.setUpdateTime(new Date()); drugMapper.insert(drug); } }参数说明Transactional注解保证事务性——如果 checkNameExist 之后的 insert 失败前面的查询不会造成数据残留。对于药品名称重复校验这里属于先校验后插入在高并发下可能会有极小的概率重复插入药房的并发量很小这种处理方式完全够用不要过度设计。4.3 库存预警与出库扣减的联动药房系统最重要的不是 CRUD而是库存和效期管理。出库操作不能只改库存数量还要生成出库记录。典型逻辑Transactional public void outboundDrug(Integer drugId, Integer quantity) { DrugStock stock stockMapper.findByDrugId(drugId); if (stock null) { throw new RuntimeException(药品库存记录不存在); } if (stock.getStockQuantity() quantity) { throw new RuntimeException(库存不足当前库存 stock.getStockQuantity()); } stock.setStockQuantity(stock.getStockQuantity() - quantity); stockMapper.updateStock(stock); OutboundRecord record new OutboundRecord(); record.setDrugId(drugId); record.setQuantity(quantity); record.setOutboundTime(new Date()); outboundMapper.insert(record); }逻辑说明这里必须事务原子化——库存扣减和出库记录写入要同时成功或同时失败。不然就会出现库存扣了但流水没记上月底对账的时候账实不符这是药房管理最怕的玄学问题。库存预警则相对简单查询所有stock_quantity warning_line的药品public ListDrugStock getWarningList() { return stockMapper.selectWarningList(); }对应的 SQLselect idselectWarningList resultTypecom.pharmacy.entity.DrugStock SELECT ds.*, di.drug_name, di.specification FROM drug_stock ds LEFT JOIN drug_info di ON ds.drug_id di.drug_id WHERE ds.stock_quantity lt; ds.warning_line ORDER BY ds.stock_quantity ASC /selectlt;是 XML 里小于等于号的转义写法直接在 XML 里写会报 DTD 解析错误这是新手容易踩的坑。LEFT JOIN可查出药品名称前端展示预警列表时不用再二次请求药品基础信息。5. 避坑记录SSM Vue 联调时最容易翻车的五个点5.1 现象前端浏览器报跨域错误Network 请求显示 CORS error原因后端接口在 8080前端页面在 8081浏览器同源策略阻止了跨域请求。很多新手直接在后端写了一个CrossOrigin解决但这样在后续部署时反而会埋隐患。解决推荐用 vue.config.js 的 devServer proxy 方案也就是本文 3.3 节写的那样。如果你要临时验证后端接口是否正常也可以在后端增加一个 CORS 过滤器但上线时要删掉。从那以后我每次都先在 vue.config.js 里配好 proxy不再动后端跨域代码。5.2 现象数据库导入 SQL 文件时中文乱码药品名称变成问号原因SQL 文件的字符集和数据库字符集不一致。Navicat 默认连接数据库时用的可能是 lantin1而 SQL 文件是 UTF-8 保存的。解决用 Navicat 打开数据库右键选择「运行 SQL 文件」在弹出窗口里选择「编码」为 UTF-8同时确认数据库字符集是utf8mb4。如果已经导入乱了把库删了重建再重新导入不要在乱码基础上手动改数据那才是真的浪费时间。5.3 现象启动项目报Unknown column update_time in field list原因实体类里有updateTime字段但数据库表里没有update_time列或者 MyBatis 的驼峰映射没开启。MyBatis 默认不会自动把updateTime转成update_time。解决在 application.yml 里开启驼峰映射mybatis: configuration: map-underscore-to-camel-case: true开启后 MyBatis 会自动把数据库的update_time映射到实体类的updateTime属性。如果开启后还报错直接用 SQL 工具检查表结构看字段到底叫什么名字。5.4 现象Maven 依赖红色报错spring-boot-starter和 SSM 版本冲突原因本地 Maven 仓库里可能同时存在不同版本的 jar 包IDEA 选择依赖时出现了冲突。最常见的是mybatis-spring-boot-starter内部依赖的mybatis-spring版本和手动引入的 SSM 版本不一致。解决pom.xml 里不要同时手动引入spring-context、spring-webmvc这些和 SpringBoot 重叠的依赖交给spring-boot-starter-parent统一管理版本。如果已经引入了删掉手动依赖让 SpringBoot 的 starter 自动装配。Maven 依赖冲突是 Java 项目里的经典玄学问题解决办法就一个原则——能用 starter 管理就不用自己写版本号。5.5 现象前端打包部署后接口请求 404刷新页面也 404原因前端npm run build打包出来的 dist 目录部署到 Tomcat 后路由跳转到子页面时刷新Tomcat 找不到对应的路径触发 404。同时 /api 请求没有正确代理到后端。解决Vue 前端需要配置路由的 history 模式后端 fallback。如果你是在同一个 Tomcat 下部署把dist目录里的内容放到webapps/ROOT下后端接口也部署在同一个 Tomcat前端请求用相对路径/apiTomcat 直接能找到后端应用。如果是前后端分服务器部署用 Nginx 反向代理把 /api 前缀转发到后端的 8080 端口这是最省心的做法。6. 继续扩展给系统补上过期预警和采购入库两个实用模块这套源码核心功能完整但药房场景里还有一个高频需求是过期药品预警——药品快过期但库存还有不少如果不提醒过期后只能报废损失不小。在现有表结构上扩展这个功能不复杂关键就是一条 SQLSELECT di.drug_name, ds.expired_date FROM drug_stock ds LEFT JOIN drug_info di ON ds.drug_id di.drug_id WHERE ds.expired_date BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL 30 DAY) ORDER BY ds.expired_date ASC;这条 SQL 把未来 30 天内到期的药品全部捞出来。参数INTERVAL 30 DAY是预警窗口改成 90 就提前一个季度提醒药房一般 30 天比较合理。在后端加一个接口返回这个列表前端用弹窗或横幅展示这个小功能做出来后整套系统的实用性会提升一个档次。另一个值得加的是采购入库功能。现有的入库记录表inbound_record已经存在但可能没有 UI 操作层。思路是一个入库表单选择供应商、选择药品、填数量提交后同时更新inbound_record和drug_stock两张表。还是那句话必须加Transactional不然就会出现入库表有流水但库存没加上去的翻车现场。Transactional public void inbound(Integer drugId, Integer supplierId, Integer quantity) { DrugStock stock stockMapper.findByDrugId(drugId); if (stock null) { stock new DrugStock(); stock.setDrugId(drugId); stock.setStockQuantity(quantity); stock.setWarningLine(50); stockMapper.insert(stock); } else { stock.setStockQuantity(stock.getStockQuantity() quantity); stockMapper.updateStock(stock); } InboundRecord record new InboundRecord(); record.setDrugId(drugId); record.setSupplierId(supplierId); record.setQuantity(quantity); record.setInboundTime(new Date()); inboundMapper.insert(record); }逻辑说明这里要注意drug_stock表可能没有对应药品的库存记录——首次入库时需要新建一条而不是直接 update 报空指针。warningLine设默认值 50后续可以在药品编辑页面里改成自定义值。从那以后我每次拿到类似的 SSM Vue 源码都会先强制走一遍「数据库导入 → 后端启动 → 前端启动 → 登录 → 查数据」的流程确认核心链路通了再去改业务。项目本身能不能用、怎么扩展跑一遍心里就有底了后面改起来也不会偏离主结构。这份源码对想练手 Java 前后端分离项目的人来说是一个值得下载下来完整跑一遍的参照系希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →