最近一两个月连续有几个人来找我聊同一个题目——基于SpringBoot的药店药品库存销售管理系统。有准备毕业设计的大四学生有刚转行做Java开发想练手的同事还有一位朋友说家里开了个小药店问这东西能不能直接拿去用。这说明这个题目并不“老套”而是真的踩中了一类很典型的业务需求药品进销存管理说到底是库存和销售但如果你只把它当成增删改查来写那这个项目做出来也就只能放在毕设盘里吃灰了。这篇文章我不打算给你贴一堆源码截图而是想把整个项目从选题拆解、数据库设计、核心业务实现到打包部署、常见坑点完整地过一遍。包括为什么选择SpringBoot、库存扣减该怎么设计事务、效期预警为什么比“库存不足”更重要这些在学校里没人会主动跟你讲清楚的事情我都会按照实际开发经验展开。适合人群很明确正在为毕设选题发愁的计算机方向学生需要快速上手一个Web管理系统的初级开发者以及想把自己的小店进销存电子化的个体经营者。1. 这个项目到底在解决什么问题1.1 不要只当“库存增删改查”来写很多同学拿到“药店管理系统”这个题目第一反应就是药品表、供应商表、入库、出库、销售连起来不就是个库存管理吗这种想法没有错但格局小了。药店的库存和销售和普通超市的进销存在本质上有几个不同点这些差异才是这个系统的核心价值。首先是药品批号和效期。普通商品有保质期就够用了但药品管理要求追踪每一批药品的批号、生产日期、有效期。同一个药品可能同时存在两个批次在库一个还有两年到期一个还有三个月到期。销售时应该遵循“先进先出”也就是默认先卖效期短的那批。如果你的表设计里只有“药品名库存数量”那这个系统在真实药店场景里是跑不动的。其次是处方药和非处方药的处理逻辑。处方药销售可能需要登记购买者基本信息非处方药就直接扫码结算。这会影响销售单的数据结构设计。第三是销售退货。药品不是所有情况下都能退的尤其拆零药品后果很麻烦但退换货流程依然需要系统记录否则账目对不上。所以这个项目的正确打开方式是把它定位成一套面向药店日常经营场景的管理系统不只是管“数量”还要管“批次、效期、价格、角色、流水”。你在答辩或者项目汇报时能把这些业务差异讲出来懂行的人一下子就知道你不是只会调框架。1.2 哪些人适合参考这个项目能解决什么实际问题这个项目的直接价值有三类使用者学生毕设功能模块清晰、技术栈主流容易写出文档、画好架构图、完成答辩演示。无论是做传统单体架构还是前后端分离都有现成的扩展空间。转行开发者通过这个项目把SpringBoot MyBatis MySQL的完整流程走一遍从建表到接口联调比看十遍教程都有用。小药店老板如果只是个人使用把系统裁剪成“药品档案 进销存 效期提醒”完全可以用低价方案私有化部署替换掉手工账本。实际开发前我建议你把这个项目的边界想清楚。它的核心是“进销存”不是医疗诊断也不是医保对接。别试图往里塞过多复杂功能先把药品信息、批次库存、采购入库、销售出库、退货、客户、供应商、统计报表这些做好系统就已经很有说服力了。2. 技术选型为什么是SpringBoot这一套2.1 后端框架选择的理由如果纯粹为了完成一个Web系统市面上有无数种选择Python的Django、FlaskPHP的LaravelNode.js的Express。但为什么Java SpringBoot成了这类毕设项目的主流我认为有三个原因。第一SpringBoot降低了Spring的门槛。以前用Spring MVC做开发光是配置XML就够折腾一天的。SpringBoot通过自动配置和starter机制让你添加依赖、编写业务代码、启动项目一气呵成。对于没有太多企业级开发经验的学生来说这是最稳妥的起点。第二Spring家族生态齐全。安全方面有Spring Security数据访问有Spring Data JPA或者MyBatis模板渲染有Thymeleaf微服务还能接Spring Cloud。这意味着项目做到后期想扩展几乎不需要换技术栈顺着原来的思路加模块就行。第三就业市场契合度。企业里Java后端占比依然很高用Java做完一个完整项目简历上写起来更有底气。面试官问Spring Boot核心原理你也能结合自己写的项目讲出一些实际理解。但我要提醒一句选SpringBoot不等于无脑堆积依赖。有人喜欢把一大堆starter全加进去然后在启动类里写上各种注解看着很酷实际上很多都用不到。这个项目里web、thymeleaf或前后端分离的json处理、mybatis、mysql、validation、lombok这几个核心依赖就够用其余用到再加不要让依赖清单变成装饰品。2.2 数据访问与权限方案的取舍持久层框架上这个项目我推荐MyBatis或者MyBatis-Plus理由很实在手写SQL可控性强复杂统计查询清楚易查MyBatis-Plus还能帮你省掉大量单表CRUD。很多互联网公司也用MyBatis这套贴近实际工作。JPA不是不能用但对于这种以表单和报表为主的管理系统JPA的隐式查询规则反而容易让人绕弯子。比如一个简单的多表统计用JPA写QueryDSL或者JPQL对于新手来说没有直接写SQL直观。所以数据访问层选MyBatis-Plus是个既高效又能讲明白的选择。权限方案上很多课程设计的做法是直接在拦截器里判断session里的用户类型。这个方案简单、好演示够用。如果你想让项目更有深度可以用Spring Security JWT来实现登录认证和接口鉴权但都要考虑时间和调试成本。我做这个项目时采用的方案是Shiro被Spring Security抢了风头实际使用Spring Security更主流但配起来有一点门槛。如果你时间紧张想先把业务跑通可以先用拦截器做一套简单的登录校验把Spring Security作为后期的加分项写进文档里声明是“为了简化演示在实际生产环境可替换为Spring Security”。这样在答辩时反而能体现出你的思考。2.3 前端方案模板渲染还是前后端分离这类管理系统的前端方案大致分成三种传统方式SpringBoot集成Thymeleaf服务端渲染页面简单直接适合快速演示和维护毕设里最常见。半分离页面还是模板渲染但表格数据和部分交互通过AJAX调用后端接口返回JSON体验更顺滑。全分离前端Vue Element UI或Layui后端只提供RESTful API开发成本高边界清晰答辩亮点足。我个人对毕设的建议是如果只求安稳交付选第一种或第二种。服务端渲染能少写很多跨域、Token、前端构建的代码出现问题也好排查。我当时采用的思路是登录页、主框架用Thymeleaf直接渲染具体业务页面里的列表数据走异步接口折中一下既保证了开发速度又保证了前端交互不是死板的整页刷新。如果你想走Vue分离路线也不是不行但你需要额外处理前端安装npm依赖、打包发布、后端跨域配置、接口前缀规划、Token传递。这些对于刚接触JavaWeb的人来说很可能成为一个巨大的时间黑洞。先把这个矛盾放在心里所有技术方案都是在时间、精力、展示效果之间做权衡不存在唯一正确答案。3. 核心功能拆解与数据库设计3.1 角色权限与三大功能域药店管理系统的用户角色至少要分成三类管理员、收银员、仓库管理员。管理员拥有全部权限收银员只能使用销售、退货、客户查询仓库管理员负责采购入库、库存调整、供应商维护。之所以强调角色拆分不是故意增加复杂度而是这是实际业务操作的底线要求。收银员如果既能卖药又能随意改库存数量月底账目出了差错就根本说不清是漏记还是人为篡改。角色权限设计得当也是后期写项目文档时一个非常重要的亮点模块。在实现上最简单的做法是在用户表加一个role字段然后通过拦截器去匹配请求路径前缀。比如/api/admin/、/api/staff/、/api/store/**分别在登录校验里检查角色。这种方案虽然没有做细到按钮级别的权限控制但对于课程设计和大多数内部管理系统的真实需求来说已经够用了。3.2 药品主档与批次库存表设计这是整个系统最核心的部分。药品档案表我用的是medicine_info常见字段包括药品名称、通用名规格、剂型、单位生产厂家批准文号零售价、进货价处方药标志Y/N拆零标志Y/N真正的库存表并不适合直接把数量挂在药品主档上因为一个药品会有多个批次。推荐把库存设计成两层的结构第一层是medicine_stock保存药品当前总库存、预警库存下限、最近入库时间。第二层是medicine_batch保存该药品下每一个入库批次的批号、生产日期、有效期、剩余数量。这样设计的核心好处是销售时你可以按照“效期最早的批次优先出库”的策略逐批次扣减数量查询时又能通过总库存表做统一的库存预警。模拟一个场景药店进了一批阿莫西林批号A有效期到明年3月数量500盒。两个月后又来了批号B有效期到后年4月数量300盒。如果系统没有批次表你在出库时只知道总数800盒根本不清楚先出哪批货。有了批次表在销售出库时先扣批号A的库存批号A卖完再自动转批号B这就是“先进先出”的真实落地。有效期预警的处理上我建议在药品批次表里增加一个expire_alert_days字段默认是30天。每天定时任务去扫一遍把有效期剩余天数小于预警天数的批次筛选出来在系统首页放一个“即将过期药品”的提醒列表。这比单纯的库存不足提醒更有价值因为药品过期造成的损失通常比缺货更严重。3.3 销售与退货流程主从表结构销售模块我采用的是“销售单主表 销售单明细表”的经典结构。主表保存本次销售总流水号、收银员、会员、实收金额、折扣金额、销售时间。明细表保存每一条售出药品的药品ID、批次ID、数量、单价、小计。为什么要拆表因为一次销售对应多条药品记录如果只在同一张表里堆字段每次查一张销售单据都会非常臃肿而且统计每笔订单总金额时还得在应用层做二次计算。拆成主从两张表后查询订单关联明细、统计时段销售额都变得很自然。销售的核心操作就是“两张表同时落库并且同步扣减库存”。这句话听起来简单但很多新手会踩进一个坑先往销售明细表里插入数据再更新库存最后发现库存更新失败但销售记录已经写进去了。这就会造成超卖和账目不一致。解决办法就是事务。把整个销售流程包在一个Transactional方法里任何一步抛出异常所有数据库操作都回滚保证数据原子性。退货则是销售流程的逆操作。用户选择原销售单选择需要退回的药品和数量系统恢复对应批次的库存同时生成一条负数金额的退货单以便月底对账。这里有个业务细节退货单价应以原销售单价为准而不是取药品档案里的最新零售价。药品是可能调价的如果按当前价格退款顾客和药店的账都容易出问题。3.4 供应商与采购入库流程除了销售端采购入库是库存数据的上游。供应商表单独建一张关联联系人、电话、地址、合作状态。采购入库单的基本流程如下新建采购单选择供应商、填写预计到货时间添加需要入库的药品及数量、进价、生产批号、有效期提交后生成待入库记录执行入库时为当前药品批量创建或更新批次表同时增加总库存记录操作人、入库时间形成采购流水这套流程的现实意义是财务需要知道每一批货的进价仓库需要知道每一批货的入库来源。日后如果某一批药出问题通过批号就能反向追踪是哪位供应商、哪个入库单送的货。如果你只写一个简单的“采购登记表”把这层追踪关系丢掉那项目质量就会大打折扣。4. 实操过程与核心代码实现4.1 本地环境准备与工程初始化我开发的本地环境是这个组合JDK 8如果你用Spring Boot 2.x则完全兼容Maven 3.6以上MySQL 5.7或8.0IDE用的IDEA Community版够用还免费。SpringBoot版本选的是2.7.x这个版本非常稳定网上的资料也最多。Spring Boot 3虽然发布了很久但要求JDK 17以及会影响一些包的命名和配置对于毕设项目并不是必需。初始化工程的话最简单的办法是去Spring Initializr网页生成一个基础工程。选好Maven、Java版本、Spring Boot版本依赖上把Web、MyBatis或MyBatis-Plus、MySQL Driver、Validation、Lombok勾上。下载解压后IDEA直接打开把Maven仓库指向阿里镜像让依赖下载快一些。4.2 建表SQL与实体生成的顺序问题建表顺序有个讲究先建基础字典表用户、角色、供应商、药品分类再建主业务表药品、批次库存、客户最后建流水表采购单、销售单、退货单。这个顺序不会遇到外键引用问题也方便你逐层测试。核心建表语句可以精简成这样去理解CREATE TABLE medicine_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, medicine_name VARCHAR(100) NOT NULL, common_name VARCHAR(100), specification VARCHAR(50), dosage_form VARCHAR(20), manufacturer VARCHAR(200), approval_number VARCHAR(50), retail_price DECIMAL(10,2), purchase_price DECIMAL(10,2), is_prescription TINYINT DEFAULT 0, is_split TINYINT DEFAULT 0, status TINYINT DEFAULT 1 ); CREATE TABLE medicine_batch ( id BIGINT PRIMARY KEY AUTO_INCREMENT, medicine_id BIGINT NOT NULL, batch_no VARCHAR(50) NOT NULL, production_date DATE, expire_date DATE NOT NULL, stock_quantity INT DEFAULT 0, purchase_price DECIMAL(10,2), INDEX idx_medicine (medicine_id), INDEX idx_expire (expire_date) );实体类与数据库表字段对应的时候我强烈建议开启MyBatis-Plus的下划线转驼峰配置。比如数据库字段expire_dateJava实体里写成expireDate代码更清爽查询结果映射时无需手工指定Column。4.3 核心业务代码的几个关键点登录校验拦截器是每一个管理和查询接口的第一道关卡。我的实现思路是登录成功后在Session里放一个loginUser对象拦截器里判断请求路径是不是白名单如果不在白名单且Session为空则跳转登录页或直接返回401。为了减少代码量我用了Java注解配合HandlerInterceptor把需要管理员权限的接口标上自定义注解。这样接口列表上你能直观看到权限要求代码也显得有条理。库存扣减是销售模块里最值得好好写的一段。以MyBatis-Plus实体操作为例我觉得还是用SQL比较踏实。销售出库时先按效期升序取该药品的批次列表SELECT * FROM medicine_batch WHERE medicine_id ? AND stock_quantity 0 ORDER BY expire_date ASC FOR UPDATE;FOR UPDATE的作用是对这些查询到的批次行加锁防止多个收银员同时卖同一个药品时出现并发扣减。然后逐条扣减直到数量满足扣完后再更新总库存。整个方法上加Transactional保证事务的原子性。如果你不讲并发这条SQL可能用不到但如果你把这条写进答辩文档面试官和老师对你的印象会完全不一样。销售报表的统计也是这个项目里很有看点的地方。按天统计销售额可以直接用日期函数分组SELECT DATE(sale_time) AS sale_day, SUM(real_amount) AS daily_sales FROM sale_order WHERE sale_time ? GROUP BY DATE(sale_time) ORDER BY sale_day;如果需要统计某个时间段内的药品销售排行可以把销售明细表和药品表做关联按药品分组汇总数量。4.4 打包与运行部署别再直接Main方法演示开发调试的时候你直接运行main方法没问题但最后交付项目或者演示的时候最好做成可执行jar包这样显得更专业。打包需要在pom.xml里确保有SpringBoot的Maven插件。然后在IDEA右侧点执行package或者用命令行mvn clean package -DskipTests执行完成后在target目录下会生成一个xxx.jar文件。部署时只需要在服务器上安装好JDK和MySQL然后运行java -jar xxx.jar这里要特别提醒一件事application.yml里的数据库连接地址最好不要写成localhost而是写成MySQL服务所在的IP地址并设置好数据库账号密码。我把配置文件拆成了application-dev.yml和application-prod.yml两份本地调试用dev部署演示用prod切换通过启动参数--spring.profiles.activeprod指定。这个习惯看着不起眼但能避免很多“本地好好的换环境就崩”的尴尬。5. 常见问题与排查技巧实录5.1 真实开发遇到的高频问题速查表老祖宗说“程序员的日常就是处理异常”这个项目也一样。我把自己实际调试过程中遇到的典型问题做成了一个速查表希望能帮你少走弯路。现象根本原因处理建议启动时提示端口被占用8080被其他应用占用修改application.yml中server.port或查找占用进程并结束数据库连接失败MySQL服务没启动、账号密码错、URL没写对先手动用客户端连一次把连接参数确认好再改配置前端页面中文乱码请求和响应编码不一致检查过滤器里是否设置了CharacterEncodingFilter前端模板文件保证UTF-8编码控制台打印警告“不推荐使用root账号”权限配置未做数据库账号隔离建一个专用的业务数据库和最小权限账号虽然本地演示没影响但值得做对MyBatis找不到XML里的SQLMapper接口和XML路径不匹配确保application.yml里配置了mapper-locations: classpath*:mapper/*.xml点击查询报Invalid bound statement方法名与XML中SQL的id不一致对比Mapper接口方法名和XML里的id一字不差才行最诛心的一个坑是数据库时区问题。连接MySQL 8时如果URL里没有加serverTimezoneAsia/Shanghai应用可能能启动但查询时间字段会出偏差。如果时间和本地时钟对不上先检查这行配置别一顿折腾去改代码。5.2 关于并发扣库存的实战经验前面提到了并发扣库存这里我想单独展开说说因为这是很多后端项目都会遇到的一道坎。你一定会碰到的情况是库存只有5盒但两个顾客同时下单买3盒而系统最后可能卖出去6盒库存变成负数。原因很简单两个请求同时读到库存为5各自扣成2最后写回的时候后写回的覆盖了先写回的。解决办法至少有三种思路。第一种是数据库行锁就是我们上面说的FOR UPDATE简单有效但对数据库性能有一定影响。第二种是乐观锁在批次表或库存表加一个version字段更新时检查version是否等于刚才读到的值不等则重试。第三种是应用层加锁比如按药品ID加分布式锁或synchronized块但只适合单机部署。我不建议你在这上面花太多时间做非常复杂的架构设计因为对毕设而言能准确说出“用行锁加事务解决超卖”就已经非常加分。如果你还面试过公司这个问题是必考考点能讲明白的人确实不多。5.3 文档与答辩环节的细节建议项目交付包含文档这是很多学生忽略但其实非常重要的一环。文档不是把代码贴上去凑字数也不是把每个类的方法抄一遍。应该重点写清楚系统背景与需求分析、功能模块图、数据库E-R图与表设计说明、核心业务流程销售、入库、退货、系统测试用例与结果。答辩时我建议你准备一条“Bug修复记录”讲一个自己实际遇到并解决的问题。比如“效期预警不触发”“销售单删除后库存不恢复”把这个问题的排查过程完整讲出来比报出十个功能点更有说服力。因为老师听起来会觉得你确实是亲手写出来的而不是从网上拉了一套现成源码改个名字就交差。另外如果你的系统里加了一些“计划外的功能点”比如首页销售趋势图、库存周转率统计、药品价格修改历史记录这些都会成为答辩时的亮点。虽然需求文档里没硬性要求但它们能证明你在设计时考虑了业务方的真实运营需要。最后说点个人体会这个系统我前前后后从设计到跑通用了大概两周时间中途推倒过一次数据库设计就是因为一开始没考虑药品批次后来发现销售逻辑没法落地。每次有人问我“这个项目应该从哪儿开始写”我都会说先把数据库表想清楚表设计对了业务代码就是水到渠成的事情而不是上来就调框架。还有个小技巧想分享一下——就是给系统的关键操作都加“操作日志”。我在药品库存调整和销售退货这类功能上统一加了一个日志表记录操作人、操作类型、前后数量、时间。一开始觉得是额外工作量后来发现调试问题时异常好用写文档时还能多写一节“系统日志审计模块”一举两得。你在做的时候也可以试试加一个轻量级的日志记录并不难但效果真的很好。这个项目复杂吗不复杂。但能把它讲清楚、做完备、部署好其实也不简单。希望你看完这篇分享后能少踩一些我踩过的坑把一个扎实的药店管理系统真正掌握在自己手里。