SpringBoot药店管理系统:Java项目源码启动与前后端联调实践
2026/9/14 8:12:20 网站建设 项目流程

简介:一套面向计算机、电子信息工程等专业学生的基于SpringBoot的药店管理系统源码,适合作为高分毕业设计、课程设计或期末大作业。压缩包内文件总计386个,整体大小15.21MB,涵盖Java后端代码、Vue前端页面、XML映射文件、数据库脚本、可执行批处理脚本及说明文档,结构划分清晰,便于按需查阅。项目基于JDK1.8、SpringBoot、Mybatis、MySQL 5.7,采用B/S架构与MVC设计模式,实现药品管理、库存管理、销售管理等核心业务,完整展现前后端分离系统的开发与部署流程。已有177人学习浏览,源码均经过严格测试,可直接导入常见开发工具运行,并附有环境配置说明。博主提供使用答疑,适合需要完整项目参考、快速完成毕设或深入理解SpringBoot实战的学习者。

1. SpringBoot 药店管理系统:一份能跑起来的 Java 项目源码

这是一个 Java 药店管理系统,压缩包里面不是单个 class 文件,而是一套完整的 SpringBoot 后端加 Vue 前端 dist 资源。它解决的场景很具体:药店每天需要维护药品分类、商品信息、库存数量和价格,手工记账容易出错,这套系统通过 B/S 结构让操作员在浏览器里完成管理,后台用 MySQL 存数据。对于做毕业设计或课程设计的计算机专业学生,这份代码的价值在于启动链路完整,从 install 到 run 都有脚本说明;对正在走 Java 学习路线的人,它又是一份可以对照看的 SpringBoot + Mybatis 项目。下面先以压缩包里的文件为入口,把启动链路讲清楚。

2. 源码包结构与启动链路:三个 bat 脚本背后的 SpringBoot 约定

2.1 压缩包文件清单与 Vue 打包产物

先看压缩包根目录:1-install.bat2-run.bat3-build.bat三个批处理文件,app.bb5659c3.csschunk-vendors.1f0a25b2.cssstyle.css是打包后的样式文件,index.html是页面入口,favicon.ico是浏览器标签页图标,必读推荐.docx说明文档。

要理解这套资源,得先分清「前端打包产物」和「后端工程」两部分。app.bb5659c3.csschunk-vendors.1f0a25b2.css这种带 hash 的文件名是 Vue 执行npm run build之后生成的,chunk-vendors通常是第三方依赖合并包,app是业务代码包;index.html引用它们。SpringBoot 默认会把classpath:/static/下的文件映射到根路径,所以把整个 dist 目录内容复制到src/main/resources/static/,浏览器访问http://localhost:8080/就能直接看到页面。这种做法的好处是不需要单独部署 nginx,Tomcat 内嵌在 SpringBoot 进程里,省去一道联调配置。

再看 bat 文件。Windows 下直接双击,顺序一般为:先1-install.bat做初始化,然后3-build.bat编译打包,最后2-run.bat启动。因为文件命名是 1、2、3,容易让人误以为执行顺序也是 1、2、3,实际上从动作看,3-build 应该夹在 install 和 run 之间。我用一个表格把它们的典型职责列出来:

脚本典型内容执行结果
1-install.batmvn install、创建数据库、导入 SQL本地 Maven 依赖就绪,数据库表结构存在
3-build.batmvn clean package -DskipTeststarget 目录下生成可执行 jar
2-run.batjava -jar target/xxxxx.jarSpringBoot 启动,Tomcat 监听 8080

需要注意,install 放在第一位,并不代表它只能执行一次。如果切换了数据库连接地址或修改了依赖,重复执行 install 也不会出错,只是会把已有 jar 重新安装到本地 Maven 仓库。真正每次改代码都要跑的是3-build.bat

2.2 环境对齐与环境变量配置

这套系统的运行环境是 Windows 或 Mac,但 bat 脚本只支持 Windows。Mac 用户直接把命令拆成三步即可。先检查版本是不是目标组合:

java -version mvn -v mysql --version

java -version能看到1.8.0_xxx说明 JDK 8 已生效;mvn -v显示 Maven 3.6 左右;mysql 命令行能连上 5.7 实例。如果终端提示找不到命令,优先检查 JAVA_HOME 和 MAVEN_HOME。实际部署时发现,jdk 装好但命令行无效,绝大多数是环境变量没配对,或者 Path 里没有加%JAVA_HOME%\bin。这是 Java 基础里最不值得浪费时间的坑,第一次配好后建议用echo %JAVA_HOME%确认。

环境确认没问题后,进入项目根目录执行初始化:

1-install.bat

如果是 Mac 或 Linux,在终端里执行等价命令:

mvn install -DskipTests mysql -uroot -p < sql/init.sql

-DskipTests跳过测试用例,避免因为环境差异导致某些集成测试失败,毕业设计阶段没有必要跑完整测试套件。如果 Maven 下载依赖很慢,修改~/.m2/settings.xml,把 central 镜像换成阿里云https://maven.aliyun.com/repository/public,这一条对国内环境几乎是必做项。

2.3 JDK 1.8、Maven 3.6、MySQL 5.7 的匹配关系

这套工程为什么没有采用更新的 JDK 17 或 MySQL 8?核心原因是依赖栈。SpringBoot 2.x 系列基于 JDK 8 编译,内嵌 Tomcat 8/9,Mybatis 的 starter 在这个组合下兼容性最好;MySQL 5.7 的默认认证插件是mysql_native_password,恰好是旧驱动支持的方式。如果把 MySQL 升到 8.0,驱动类要从com.mysql.jdbc.Driver改成com.mysql.cj.jdbc.Driver,连接串还必须加时区参数,否则 SpringBoot 启动会直接报Server returns invalid timezone。这些一眼看不出来的问题,会在后面排错章节单独说。

一个典型application.yml里的数据源配置通常这样写:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/pharmacy?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.pharmacy.entity

characterEncoding=utf8是中文不乱码的关键,useSSL=false避免本机 MySQL 没有 SSL 证书时的握手警告,serverTimezone只对 MySQL 8 必要,5.7 下无所谓。mapper-locations指向 XML 文件,type-aliases-package让 XML 里可以直接写Medicine而不是全限定类名。这两个配置会影响第三章的 Mapper 写法。

3. SpringBoot 后端分层:从 Controller 到 Mapper 的药品管理流程

3.1 MVC 分层与请求走向

这套药店系统的后端按 SpringBoot 最常见的三层结构组织:Controller 负责接收 HTTP 请求和返回 JSON,Service 处理业务规则,Mapper/DAO 负责和 MySQL 做数据交换。再加上 Mybatis 的 XML 映射文件,SQL 可以脱离 Java 代码单独维护。这个结构在 Java 面试题里属于高频考点,面试官通常会问「一次请求从浏览器到数据库经历了什么」。答案并不复杂:浏览器通过 Ajax 请求访问 Controller 的 URL,Controller 调 Service,Service 调 Mapper,Mapper 执行 SQL 返回结果,再原路返回 JSON。

药品管理功能里最核心的实体是药品信息,我通常把它命名为 Medicine。一张表的字段至少包含药品分类、编码、名称、规格、单位、库存、进价、售价、生产日期、有效期、状态。注意库存字段是业务重点,过多的前端校验不可靠,真正要控制库存的是 Service 层。比如保存订单扣库存时,用更新的 SQL 而不是先查后改,避免并发下超卖。

3.2 药品新增接口的实现骨架

不管压缩包里的包名是com.pharmacy还是com.medicine,原理是同一套。下面是我会采用的新增药品骨架,省略了部分校验逻辑:

@RestController @RequestMapping("/api/medicine") public class MedicineController { @Autowired private MedicineService medicineService; @PostMapping public ApiResponse save(@RequestBody Medicine medicine) { medicineService.addMedicine(medicine); return ApiResponse.success(); } }

Controller 负责把 HTTP 请求里的 JSON 绑定到 Medicine 对象。@RequestBody表示请求体必须是 JSON 格式,@PostMapping接收 POST 方法。返回ApiResponse是统一响应体,常见字段是codemessagedata,前端通过code === 200判断成功或失败,这样的好处是错误处理可以集中做。

Service 层承接业务逻辑,事务边界也在这里:

@Service public class MedicineService { @Autowired private MedicineMapper medicineMapper; @Transactional public void addMedicine(Medicine medicine) { if (medicine.getStock() < 0) { throw new RuntimeException("库存不能为负数"); } medicine.setCreateTime(new Date()); medicineMapper.insert(medicine); } }

@Transactional保证插入过程中任何一个步骤失败都会回滚,如果后续加入「新增药品同时记录操作日志」的逻辑,事务会把药品表和日志表的写入包在一起。库存校验放在 Service 而不是 Controller,是因为 API 层可以被多个接口调用,基础规则下沉后才能统一生效。

Mapper 接口和 XML 是 Mybatis 的核心:

@Mapper public interface MedicineMapper { int insert(Medicine medicine); }

XML 文件放在src/main/resources/mapper/MedicineMapper.xml

<insert id="insert" useGeneratedKeys="true" keyProperty="id"> insert into medicine (name, specification, unit, stock, sale_price, create_time) values (#{name}, #{specification}, #{unit}, #{stock}, #{salePrice}, #{createTime}) </insert>

useGeneratedKeys="true"配合keyProperty="id",插入后数据库自增主键会自动回填到传入对象的 id 字段。#{}是预编译占位符,能防止手工拼接 SQL 的注入问题,千万不要用${}去拼接普通参数。价格字段在数据库里建议用decimal(10,2),Java 用BigDecimal接收,避免 float 的精度丢失。

3.3 Mybatis 动态 SQL 与库存更新的常见边界

做药店系统不可能只做单表新增。药品列表页一般支持按名称模糊查询、按分类筛选、按库存预警过滤。Mybatis 的<where><if>是应对这类场景的标准写法:

<select id="selectPage" resultType="Medicine"> select id, name, specification, unit, stock, sale_price from medicine <where> <if test="keyword != null and keyword != ''"> and name like concat('%', #{keyword}, '%') </if> <if test="categoryId != null"> and category_id = #{categoryId} </if> <if test="lowStock != null and lowStock"> and stock &lt; 20 </if> </where> order by id desc </select>

<where>标签会自动去掉第一个条件前面的and<if>相当于动态拼接字符串。注意&lt;是因为 XML 里<必须转义,很多新手在这个位置被坑过,把stock < 20写进 XML 会导致解析失败。

库存更新建议使用 SQL 表达式,而不是先查询再赋值:

update medicine set stock = stock + #{count} where id = #{id} and stock + #{count} >= 0

这条 SQL 把判断条件和更新放在同一个语句里,在高频操作下比「select 再 update」稳定得多。如果后续要支持多门店和更复杂的并发控制,再考虑引入乐观锁版本号。

4. Vue + Ajax 页面联调:静态资源挂载与接口对接

4.1 SpringBoot 如何托管 Vue 打包资源

这一章进入 B/S 架构的另一面。SpringBoot 对静态资源有默认规则:classpath:/static/下的index.html就是访问/的页面;浏览器加载index.html后,会自动请求其中的/css/app.bb5659c3.css/js/chunk-vendors.1f0a25b2.js等文件,SpringBoot 会自动到/static/查找。因此,拿到压缩包里的前端资源后,正确操作是把这些文件全部放到src/main/resources/static/目录下,再执行3-build.bat重新打包。直接双击index.html打开是看不到数据的,因为这时的接口域名还是http://localhost:8080,而文件协议下跨域请求会被浏览器直接拦截。

这也解释了一个常见现象:为什么压缩包里有两个index.html。一个可能是 Vue 项目根目录的源入口,另一个是打包要用的 dist 入口。实际运行时以后者为准,如果修改了前端代码重新 build,只覆盖 dist 里的静态目录即可。

4.2 Ajax 调用后端接口的典型写法

前端页面不外乎做三件事:加载药品列表、提交新增表单、完成入库出库按钮。下面的 Vue 实例展示了一个列表加载的常见写法:

new Vue({ el: '#app', data: { medicines: [], keyword: '' }, mounted() { this.loadMedicines(); }, methods: { async loadMedicines() { const params = new URLSearchParams(); params.append('page', 1); params.append('size', 10); params.append('keyword', this.keyword); const res = await fetch(`/api/medicine/list?${params.toString()}`); const data = await res.json(); if (data.code === 200) { this.medicines = data.data.records; } } } });

mounted是 Vue 生命周期中页面渲染完成后执行的回调,在这里发起请求能保证 DOM 已经准备好。fetch是浏览器原生接口,不需要引入 axios,适合毕业设计这种尽量少依赖的场景。后端返回格式如果包含code/data/records这种包一层分页结果的结构,往往是因为引入了 PageHelper 做分页。data.data.records两层 data 会让新手困惑,实际第一层是接口统一包裹,第二层才是分页对象。

这套系统里我常把接口收敛成下面表格里的三类,前端照着这个路径对接就不会乱:

请求路径方法说明核心参数
/api/medicine/listGET分页查询药品列表page、size、keyword、categoryId
/api/medicinePOST新增药品JSON 格式 Medicine
/api/medicine/stockPUT库存变更id、count、type

type通常用 0 表示出库,1 表示入库。后端根据 type 决定是stock - count还是stock + count,这个参数在库存变更日志里也会记录下来。

新增药品时需要注意请求头的 Content-Type,用 fetch 时要显式写:

await fetch('/api/medicine', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: '阿莫西林胶囊', specification: '0.25g*24', stock: 100, salePrice: 12.50 }) });

JSON.stringify把对象转成字符串,SpringBoot 的@RequestBody才能正确反序列化。如果漏掉Content-Type: application/json,后端会把 body 按表单解析,@RequestBody会收到空对象或直接报 415 错误。

4.3 联调时最容易出现的跨域问题

本地开发时,如果 Vue 跑在 8081 端口,SpringBoot 跑在 8080 端口,前端请求就会出现 CORS 跨域。对于这套代码,如果最终是前后端一起打包到 SpringBoot,根本不会触发跨域,保持一个端口最省事。只有当你在 IDEA 里同时启动 Vue dev server 和 SpringBoot 时才需要处理。

我一般推荐用代理方式解决,修改 Vue 的vue.config.js

devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

/api开头的请求会被代理转发到 8080,浏览器看到的是同源请求,便不会拦截。如果是后端单独部署到线上,也可以在后端统一加跨域配置,但只适合开放测试接口,生产环境尽量保持同源,少开跨域。

5. 启动后验证与排错:从日志到接口测试的关键动作

5.1 启动失败先看这三个症状

运行2-run.bat如果启动失败,第一个动作是看控制台最后 20 行,而不是重新打包。最常见的三类错误:

  • Access denied for user 'root'@'localhost':数据源账号密码和application.yml不一致。
  • Port 8080 was already in use:之前启动过的 Java 进程占用端口,Windows 下用netstat -ano | findstr 8080看 PID,再taskkill /F /PID <pid>
  • Table 'pharmacy.medicine' doesn't exist:说明1-install.bat没有成功建表,手动执行 SQL 初始化。

对于后两类,日志里会有明确异常栈,把关键行复制出来搜索,比盲改配置高效。

5.2 用 curl 验证接口是否可通

启动成功后,先不急着打开页面,用 curl 把接口链路验证一遍。下面是一条新增药品的测试命令:

curl -X POST http://localhost:8080/api/medicine \ -H "Content-Type: application/json" \ -d '{"name":"感冒灵颗粒","specification":"10g*9袋","stock":50,"salePrice":19.80}'

返回{"code":200,...}说明后端链路正常。再访问curl http://localhost:8080/api/medicine/list?page=1&size=10验证分页查询。如果新数据能查到,再打开浏览器看页面,这样能直接分清问题在前端还是后端。

5.3 一次替换默认环境配置的小技巧

拿到源码后通常要改三件事:数据库密码、端口号、文件上传路径(如果有)。不要只改一处而忽略打包后的 jar。SpringBoot 支持启动参数覆盖配置:

java -jar target/pharmacy.jar \ --spring.datasource.password=新密码 \ --server.port=9000

这种方式适合临时验证。需要长期生效时,把配置写到外部application.yml,放在 jar 同目录的config/下,SpringBoot 会优先读取外部配置,避免每次打包都改代码。SpringBoot 的配置优先级是config/目录下配置 > jar 同目录配置 > jar 包内部配置,启动日志里加载了哪个文件会明确打出来,再结合curl http://localhost:9000/api/medicine/list?page=1&size=1的返回结果,即可确认外部配置已经生效。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询