SSM+Vue药房管理系统源码落地与避坑指南

发布时间:2026/10/7 18:43:46
SSM+Vue药房管理系统源码落地与避坑指南 简介这是一份基于Java与Vue技术栈的药房药品管理系统实现源码面向需要学习SSM、SpringBoot前后端分离开发的Java学习者以及正在筹备药品管理类课程设计或毕业设计的开发者。系统完整展现了SpringSpringMVCMyBatis整合架构在后端服务中的组织方式配合SpringBoot减少配置工作量前端采用Vue.js渐进式框架以数据驱动和组件化思路构建用户界面数据库选用MySQL 5.7及以上版本并使用Navicat或SQLyog进行数据表设计与维护项目依赖由Maven统一管理开发环境支持IDEA与Eclipse。资源包为zip压缩包大小22.7MB包内文件类型明细暂未提供适合直接导入IDE阅读与调试。目前已有83人学习适合具备Java基础、希望掌握SSM与Vue前后端分离项目整体结构的中级开发者参考。从中可学习Controller-Service-Mapper分层写法、Vue与后端接口的交互方式、MySQL数据库表设计要点以及Maven在项目管理中的实际运用有助于快速构建同类业务系统。1. 药房药品管理系统这代源码到底能给你省下什么不少人下载 java-ssmvue 药房药品管理系统源码是想三天内交出一份能演示、能答辩的完整项目。这类源码背后是一个典型的前后端分离的 Java Web 方案后端用 SSMSpring SpringMVC MyBatis处理业务前端用 Vue 2 配合 Element UI 做页面数据库落在 MySQL 上。业务范围非常集中——药品档案、供应商、入库出库、库存和效期预警、销售记录绝大多数同题源码都不出这五块。适合三类人拿它当毕业设计或课程设计的学生、想快速搭一套增删改查后台模板的初级开发、不想花几千块买商品软件的小药房。最常听到的反馈是“启动报错、页面白屏、接口连不上”这篇就按拿到手之后的完整落地路径把业务拆解、环境匹配、前后端联调、踩坑记录一次说透。2. 拆源码前先拆业务药房系统后台该有哪些表和接口2.1 从进销存反推数据库设计药房系统说到底是一个进销存系统外加一个面向药师的销售收银入口。所谓“药品管理”不能只有一张表否则库存流水、供应商关系、费用统计全是乱的。我一般先看数据库设计文件没有设计文件的源码不用着急跑自己用下面这组核心表去对照它的业务就能判断功能全不全drug_info药品主表存通用名、商品名、规格、厂家、批准文号、单位、零售价、库存数量、有效期。supplier供应商表和 drug_info 做多对一关联。stock_log入库、出库、退货的流水表每条记录有类型字段这样才能回答“这个月进了多少货、消耗了多少库存”。sale_record销售记录表至少包含药品 ID、数量、单价、总价、处方号非处方药可以没有、收银时间。user系统用户表区分管理员、药师、收银员角色。也见过把库存字段直接挂在 drug_info 里的做法这对小药房完全够用查询时少一次 join。但注意库存必须只能通过 stock_log 来变更绝不能靠前端直接改 drug_info 的 stock 字段。下面是最小可运行的建表 SQL省掉一个系统最崩溃的问题——字段类型不对导致的前端显示“NaN”或日期错乱CREATE TABLE drug_info ( drug_id INT PRIMARY KEY AUTO_INCREMENT, drug_name VARCHAR(100) NOT NULL COMMENT 药品名称, spec VARCHAR(50) NOT NULL COMMENT 规格, factory VARCHAR(100) COMMENT 生产厂家, approval_no VARCHAR(50) COMMENT 批准文号, unit VARCHAR(10) DEFAULT 盒 COMMENT 单位, price DECIMAL(10,2) NOT NULL COMMENT 零售价, stock INT NOT NULL DEFAULT 0 COMMENT 当前库存, exp_date DATE COMMENT 有效期至, supplier_id INT COMMENT 供应商ID, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE stock_log ( log_id INT PRIMARY KEY AUTO_INCREMENT, drug_id INT NOT NULL, type TINYINT NOT NULL COMMENT 1入库 2出库 3退货, quantity INT NOT NULL COMMENT 变动数量, operator_id INT COMMENT 操作人用户ID, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL, role VARCHAR(20) NOT NULL COMMENT ADMIN/DRUGGIST/CASHIER );这段 SQL 里有两个地方是后续联调的常见分歧点一是 price 用 DECIMAL 而不是 FLOATFLOAT 在金额计算上会产生 0.1 0.2 的精度错误二是 stock_log 不存药品名、只存 drug_id否则改名时历史流水全对不上。拿到别人的源码先打开建表脚本确认这两点能省后面和前端同事在两角钱误差上的拉扯。2.2 SSM 后端的包结构与接口路径约定传统 SSM 项目没有 Spring Boot 的自动配置包结构就是 Controller / Service / Mapper / Pojo 四层再加一个 resources 下的 MyBatis XML 目录。这里最容易翻车的是“Mapper 接口和 XML 映射文件没有绑定”常见的解决套路是把 XML 放到 resources/mapper 下并通过 MapperScannerConfigurer 扫描接口包。接口路径约定上我建议前端统一走 /api 前缀方便在 Web 层做请求拦截也利于以后加网关。药房系统的核心接口不外乎下面这些POST /api/login账号密码登录返回用户信息和 tokenGET /api/drug/list分页查询药品支持按名称/厂家模糊搜索POST /api/drug/addPOST /api/drug/updatePOST /api/drug/deletePOST /api/stock/in、POST /api/stock/out入库出库同时写 drug_info 和 stock_logGET /api/sale/list销售记录列表一个典型 SSM 的 Controller 长这样重点在方法返回结构上——不要直接返回 Map 拼 result而是统一成 {code, msg, data} 的 JSON 包装Vue 页面解析才省心Controller RequestMapping(/api/drug) public class DrugController { Autowired private DrugService drugService; ResponseBody RequestMapping(/list) public Result list(int pageNum, int pageSize, String keyword) { PageHelper.startPage(pageNum, pageSize); ListDrugInfo list drugService.searchDrug(keyword); PageInfoDrugInfo pageInfo new PageInfo(list); return Result.success(pageInfo); } }逻辑说明PageHelper 是 SSM 里最常用的分页插件拦截 MyBatis 执行器自动拼 LIMITPageInfo 里自带 total、pages 这些字段前端表格分页直接绑 total 就行。如果源码里没有 PageHelper就得手动写 COUNT 和 LIMIT工作量会大一些但也得能看懂。接口直接返回 pageInfo 对象由 Jackson 序列化成 JSON这意味着 pom.xml 里jackson-databind必须存在还要开启 SpringMVC 的注解驱动mvc:annotation-driven/否则返回不了 JSON这又是另一个高频坑。2.3 权限和登录怎么塞进去拦截器还是 Spring Security药房系统一般不用 Spring Security——不是不能用而是对多数源码来说太重。写论文时“Spring Security 实现权限管理”听起来不错但一旦配错过滤器链调试成本直接上升。更常见、也更稳的做法是写一个 HandlerInterceptor拦截除了 /api/login 以外的所有 /api/** 请求校验前端传过来的 session 或 token。JWT 版本的代码也就是一个工具类加一个拦截器的量public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(token); if (token ! null JwtUtil.verify(token)) { return true; } response.setStatus(401); response.getWriter().write({\code\:401,\msg\:\not login\}); return false; } }参数说明这里拦截器只做“你是不是登录了”的校验不做细粒度角色权限如果要区分“只有药师能卖药”需要在 Controller 层通过 RequiresRole 这类自定义注解再去查一次用户角色或者干脆在本次请求里把用户信息塞到 ThreadLocal 里拦截器里取出来拼到 request 属性上。很多源码为了演示省事根本不做权限只要登录成功就把所有菜单放开这在答辩时其实是一个容易被追问的软肋建议后面按上面的方式补。3. 用 IDEA 把后端跑起来版本匹配、数据库、最小启动清单3.1 JDK、Maven、Tomcat 到底该怎么选SSM 源码最常见的运行环境是JDK 8、Maven 3.6、Tomcat 8.5/9、MySQL 5.7 或 8.0。这三个版本的任何一个不匹配都可能让启动在 10 秒内失败。先说 JDK有些源码用到了javax.servlet的旧写法在 JDK 9 以上会出现模块化问题老老实实用 JDK 8。Tomcat 我推荐 8.5兼容性最稳如果用 Tomcat 10javax.servlet变成了jakarta.servlet老 SSM 项目直接启动失败。Maven 版本太高比如 Maven 4可能和 IDEA 内置绑定冲突用 3.6.3 左右即可。打开 pom.xml先确认依赖版本不要各自为政。下面是一组我多次复现过、能同时通过编译的版本坐标基准properties spring.version5.3.24/spring.version mybatis.version3.5.13/mybatis.version mysql.version8.0.30/mysql.version /properties不要小看版本号很多源码在网盘里遍历了两年换过 JDK、换过数据库但 pom 没更新导致 MySQL 8 的驱动用旧版com.mysql.jdbc.Driver直接抛出ClassNotFoundException。MySQL 5.7 用com.mysql.jdbc.Driver没错MySQL 8 必须改成com.mysql.cj.jdbc.Driver这是第一道门槛。3.2 初始化数据库并改数据库连接拿到源码后先初始化数据库。生产环境谨慎导入本地随意。常见做法是打开 SQL 文件手动建库再 source 或直接在 IDEA 里执行。命令行也可以注意指定编码否则建表注释里的中文会变成问号mysql -uroot -p --default-character-setutf8 -e CREATE DATABASE pharmacy DEFAULT CHARACTER SET utf8mb4; mysql -uroot -p --default-character-setutf8 pharmacy pharmacy.sql然后改数据库连接配置。SSM 的数据库配置一般在jdbc.properties或db.properties里改这一处就行jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/pharmacy?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingUTF-8 jdbc.usernameroot jdbc.password123456参数说明serverTimezoneAsia/Shanghai是 MySQL 8 必填不填报时区异常characterEncodingUTF-8控制数据库读写中文缺失后药品名称、厂家全是问号而且加了之后前后端都会省很多事。如果你拿到的是 MySQL 5.7 的库驱动和 URL 时区参数就在jdbc.properties里去掉或改掉不要跟数据库版本硬顶。3.3 部署到 Tomcat 并验证接口打完包的 SSM 项目在 IDEA 里通常是Maven面板先clean再package生成 war 包然后配置 Tomcat 的 Deployment 指向那个 war。如果源码里带了 Tomcat 本地配置直接用没有的话新版 IDEA 的 “Run/Debug Configurations” 里选 Tomcat Server LocalDeployment 里添加 Exploded 或 war 包设置 Application context 为/或/pharmacy。Application context 决定了前端页面里静态资源和接口请求的根路径很多联调不通都是这个没对齐。启动后不要急着点页面先用 curl 看接口。假设后端端口 8080根路径是/pharmacy验证登录接口curl -X POST http://localhost:8080/pharmacy/api/login \ -H Content-Type: application/json \ -d {username:admin,password:123456}如果返回{code:200,data:{token:...}}后端就活了。如果要访问药品列表带上 header-H token: 上一步返回的token。这一步能在 5 分钟内把后端通不通搞清楚避免前端页面白屏时还在瞎猜后端有没有起来。常见启动失败的日志Failed to scan ... classpath resource、Error creating bean with name ...多半是 MyBatis 的 mapper XML 没被扫到去检查spring-mybatis.xml里的mapperLocations路径是否和实际 XML 目录一致。4. 前端 Vue 工程环境配置、路由、对接后端4.1 Vue 2 Element UI 的工程初始化常见形态这套项目的前端原名虽然是“vue”但绝大多数源码是 Vue 2 Vue Router 3 Element UI Axios。Vue 3 的项目也会有但药房管理系统是经典毕设题资源存量的大头在 Vue 2。如果你手头的源码是 Vue 2我记得node-sass这个东西是最大的坑Node 版本 17 以上装 node-sass 大概率编译失败换 Node 14 是很多人的后悔药。安装依赖和启动命令如下npm install -g cnpm --registryhttps://registry.npmmirror.com cnpm install npm run serve参数说明cnpm只是把包下载源换成国内镜像不改任何依赖逻辑。npm run serve启动后默认端口 8080和后端 Tomcat 端口冲突很正常改vue.config.js里的devServer.port为 8081 即可。如果npm install卡住多半是下载electron或node-sass的二进制文件设置环境变量SASS_BINARY_SITEhttps://npmmirror.com/mirrors/node-sass/后重装。4.2 路由、请求封装与页面骨架前端工程的结构一般长这样src/router/index.js定义页面路由src/utils/request.js封装 axios 实例src/views/drug/DrugManager.vue是核心页面。路由要处理好登录守卫——未登录跳回登录页很多源码里干脆没有这个导致刷新页面后直接白屏。下面这段路由和请求封装可以直接抄// router/index.js import Vue from vue import Router from vue-router import Login from /views/Login.vue Vue.use(Router) const router new Router({ routes: [ { path: /login, component: Login }, { path: /drug, component: () import(/views/drug/DrugManager.vue), meta: { requiresAuth: true } } ] }) router.beforeEach((to, from, next) { if (to.meta.requiresAuth !localStorage.getItem(token)) { next(/login) } else { next() } }) export default router这里用了路由懒加载component: () import(...)打包后每个页面单独成一个 JS 文件首屏更快。关键思路是路由守卫读 localStorage 的 token——后端拦截器校验的是 header 里的 token前端在登录成功后就把它存进 localStorage。注意刷新页面时路由守卫会读到 token但 token 可能已过期这时需要 request 封装里对 401 状态统一跳回登录页。// utils/request.js import axios from axios const request axios.create({ baseURL: process.env.VUE_APP_API_BASE_URL || /api, timeout: 5000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[token] token } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } ) export default request这段代码的逻辑是所有请求自动带上 token所有响应先剥一层response.data让业务代码直接拿到{code, msg, data}对象。baseURL 用的是环境变量如果你用npm run serve开发推荐在vue.config.js里配置 devServer proxy把/api代理到后端这样既能绕开跨域又不用写死 IP。常见的问题是前端直接请求后端http://localhost:8080/pharmacy一旦后端口径变化就要改几十个文件所以 proxy 才是合理解法。4.3 药品管理页表格、弹窗、分页的一整套可抄代码药品管理页是这套系统的“门面”。一个能上演示的页面必须包含查询条件、数据表格、分页、新增/编辑弹窗、删除确认。下面是 DrugManager 里列表和弹窗的核心片段Element UI 的写法是固定的但数据绑定的细节需要仔细对照你的后端返回字段template div el-input v-modelkeyword placeholder药品名称/厂家 clearable keyup.enter.nativeloadList stylewidth: 240px / el-button typeprimary clickloadList查询/el-button el-button typesuccess clickopenAddDialog新增/el-button el-table :datadrugList border stripe el-table-column propdrugName label药品名称 / el-table-column propspec label规格 / el-table-column propfactory label厂家 / el-table-column propprice label零售价 / el-table-column propstock label库存 / el-table-column label操作 width160 template slot-scopescope el-button sizemini clickopenEditDialog(scope.row)编辑/el-button el-button sizemini typedanger clickremoveDrug(scope.row.drugId)删除/el-button /template /el-table-column /el-table el-pagination background layouttotal, sizes, prev, pager, next :totaltotal :page-sizepageSize :current-pagepageNum current-changehandlePageChange / /div /template script import request from /utils/request export default { data() { return { drugList: [], keyword: , total: 0, pageNum: 1, pageSize: 10 } }, methods: { async loadList() { const res await request.get(/drug/list, { params: { pageNum: this.pageNum, pageSize: this.pageSize, keyword: this.keyword } }) this.drugList res.data.list this.total res.data.total }, handlePageChange(val) { this.pageNum val this.loadList() } }, mounted() { this.loadList() } } /script这段代码的联调重点是res.data.list和res.data.total。后端 PageHelper 的 PageInfo 序列化默认字段是list和total如果你的后端返回的字段名是rows或count这里就要对齐。另一个细节是新增弹窗通常新增和编辑复用同一个 el-dialog通过dialogVisible和当前行数据切换 title提交时调用request.post(/drug/add, form)。删除时先this.$confirm(确认删除该药品吗, 提示, { type: warning })再做否则误删数据没有后悔药。5. 复现这套源码必踩的 5 个坑现象、原因、解法5.1 后端启动阶段的三个高发问题现象一Tomcat 启动几秒后崩掉控制台报NoClassDefFoundError或NoSuchMethodError。原因绝大多数是 Spring 和 Jackson 或 MyBatis 的版本冲突。SSM 是手工拼装的框架不同版本之间的 API 有差异比如 Spring 5 去掉了部分旧接口而源码里的 MyBatis-Spring 还停留在 1.2.x。解法是统一版本号把 Spring 提到 5.3.xMyBatis-Spring 提到 2.x然后 clean 再 package。检查依赖树用mvn dependency:tree看是否有重复的 spring-core。现象二Invalid bound statement (not found): com.pharmacy.mapper.DrugInfoMapper.selectList。原因很直接——MyBatis 的 Mapper 接口找到了但对应的 XML 没有注册进去。检查三处接口扫描包路径是否包含 Mapper 接口XML 文件是否放在resources/mapper下spring-mybatis.xml里的mapperLocations是否写对。还有一个冷门情况Maven 默认只打包 resources 目录下的文件如果你把 XML 放在java目录下会被过滤掉IDE 里能看见但 target 里不存在解法是在 pom 里加 resources 配置。现象三MySQL 连接报Public Key Retrieval is not allowed。这是 MySQL 8 的驱动在非 SSL 连接下对 caching_sha2_password 认证的一个保护。原因就是连接串里allowPublicKeyRetrieval没设。解决很简单在 jdbc.url 后面追加allowPublicKeyRetrievaltrue。如果不加连接在第一次认证时就被服务端断开本地还能看到告警到你远程部署时才报那时排错更痛苦。5.2 前后端联调阶段的两类问题现象四浏览器控制台报跨域错误Access to XMLHttpRequest ... CORS。原因是一个 8080 一个 8081端口不同就构成跨域。很多源码的后端没写跨域配置。两个解法后端写 CORS 过滤器放行所有请求适合本地开发更推荐前端在vue.config.js里配 devServer proxy由开发服务器代理/api让浏览器以为请求的是同源地址。注意配了 proxy 后axios baseURL 必须改成/api而不是http://localhost:8080/pharmacy否则 proxy 完全没生效。现象五新增药品后列表刷新不出来或者删除后返回成功但数据还在。原因多半是接口返回里的 code 判断和后端实际不一致。后端 Result 里 code 用 200 代表成功前端校验却写成了res.code 200或者后端成功返回无 data前端却在res.data.total上取数。解法是先打开浏览器 Network 看响应体把前后端的返回结构对齐。另外删除接口如果用 DELETE 方法要确保后端 Controller 的RequestMapping也声明了method RequestMethod.DELETE前后端方法不一致会直接 405。6. 不要只停在“跑起来”从可用到答辩/验收的最后一个细节验证这套系统真正可用不是点开页面看列表就完事而是要顺着一条完整业务链路过一遍用管理员登录 → 新建一个供应商 → 在药品管理里新增药品并填有效期 → 对该药品做一次入库操作 → 去库存列表确认数量增加 → 再做出库 → 库存扣减 → 查流水表能看到两条记录。这一步走通才说明核心的“进销存”闭环是真的能跑的而不是每个页面静态展示。我见过不少源码入库和出库只是改 drug_info 的 stock 字段根本没往 stock_log 写流水答辩被问“这个药品本月入库多少次”立刻哑口无言。如果还有时间建议补两个高性价比的细节。第一个是效期预警查询药品列表时把“距离保质期不足 30 天”的数据标红SQL 里一个DATE_FORMAT(exp_date, %Y-%m-%d) DATE_ADD(CURDATE(), INTERVAL 30 DAY)就能实现。第二个是库存扣减的并发安全性出库接口不要先SELECT stock再UPDATE stock而是直接用UPDATE drug_info SET stock stock - #{quantity} WHERE drug_id #{id} AND stock #{quantity}影响行数为 0 时就抛异常这一步是面试官最爱问的“超卖”问题的关键。最后说一个我自己的教训。早期帮人改这类源码我只保证跑起来结果对方答辩时被问到“库存表怎么避免负数”“药品删除后销售记录外键怎么办”卡在台上。后来再做交付我都会把事务注解Transactional加在入库、出库、销售这类写操作上并确认删除药品是逻辑删除而不会连带删流水。一套能跑、有事务、有流水的系统和一套只能拉列表的 demo在验收结果上是完全不同的东西。希望这篇落地路径能帮你把它真正做成拿得出手的作品而不是下一套“启动即失败”的网盘收藏。本文还有配套的精品资源点击获取