2026/9/19 6:07:35

SpringBoot智慧药店药品管理系统:核心设计与毕设实践指南

SpringBoot智慧药店药品管理系统:核心设计与毕设实践指南 最近在整理新一年的毕设项目资源600套项目筛下来真正让我觉得值得单独拿出来聊一聊的SpringBoot做的“智慧药店药品信息管理系统”算一个。编号14099在整批项目里属于典型的“行业属性管理系统”表面看是增删改查实际上藏着药品批号、有效期、库存流水、销售扣减这些药店业务逻辑难度又没有明显超标对准备做Java方向毕设的同学来说性价比相当高。这套系统解决的问题很具体药店里的药品到底该怎么管。普通商品只需要管数量和价格药品却绕不开批准文号、生产批号、生产日期、有效期、供应商、库存上下限、采购价和售价还得有近效期预警和低库存预警。把这些逻辑理清楚项目就有了真实业务做支撑答辩时讲“为什么这样设计”也会顺畅得多。适合三类人看一是还没定题、正在纠结做什么的Java方向学生二是已经选了管理系统方向、想把项目做出区分度的人三是想系统学习SpringBoot项目结构但又不想看一堆脱离业务的demo代码的开发者。下面我按实际做项目的顺序来拆选题思路、数据模型、核心实现、排错经验、答辩准备一路讲到底。1. 选题思路与整体设计为什么“智慧药店”值得做1.1 同类管理系统对比药店这个主题差在哪管理系统类毕设年年都有图书管理、学生管理、仓库管理早就被写烂了。老师看到这类题目基本没有新鲜感答辩时问题也容易往深处扎“你的系统解决了什么实际问题”“库存是怎么扣的”“并发同时下单会不会出错”这些追问很多同学在普通CRUD项目里根本没有考虑过。药店药品信息管理不一样它的业务约束天然比普通商品管理复杂。第一药品档案字段多而且有行业规范批准文号、生产批号、规格、单位、厂家、供应商都得有还必须带有效期。第二库存进出要讲究批号和效期不能像普通库存那样随便扣一个数销售时一般按“近效期先出”的原则来处理。第三药店信息化有合规的影子低库存要预警近效期要提示过期药品不能销售这些约束恰好是项目深度的来源。所以同样是SpringBoot写一套系统选药店这个主题业务复杂度恰到好处。不会简单到没东西可讲也不会复杂到短时间做不完。对毕设来说这是很合适的“中间难度”题目。1.2 功能模块划分先跑通主链路再来谈大而全刚拿到这种题目很容易犯的一个错是一上来就想着把会员管理、统计报表、电子处方全塞进去最后每个模块都做了一半没有一个能完整讲清楚。我的建议是先圈定核心闭环跑通之后再扩展。核心模块可以这样划分基础数据药品分类、药品厂家、供应商。药品档案药品编码、名称、规格、批准文号、批号、生产日期、有效期、采购价、售价、库存上下限、上下架状态。采购入库入库单、入库明细入库后自动增加对应药品的库存并写一条库存流水。销售出库销售单、销售明细逐条校验库存和有效期扣减库存并写流水。库存预警低库存药品列表、近效期药品列表。系统管理用户登录、角色权限、修改密码。第一版先把“药品档案—采购入库—销售出库—库存预警—库存流水”这条主链路跑通。统计报表、会员管理、处方登记都是增量功能等主链路稳定了再加整体节奏就不会失控。“智慧”这两个字不需要靠炫酷界面去体现数据完整、流程严谨、预警及时就是最实在的智慧。1.3 技术栈选型这套组合为什么不容易翻车SpringBoot不用多说Java毕设的绝对主流。它内嵌Tomcat不需要单独部署容器自动配置帮我们省掉大量XML配置一个jar包就能跑。版本选择上要特别注意如果本地是JDK 8就用SpringBoot 2.7.x如果能装JDK 17或21直接用3.x也没有问题。网上很多“项目跑不起来”的求助八成是版本错配导致的这一点第三章会详细排雷。持久层我推荐MyBatis-Plus而不是原生MyBatis。单表CRUD不需要手写SQL内置BaseMapper直接用内置分页插件也相当方便。药品管理这种业务单表操作占大多数MyBatis-Plus能把开发量砍掉三分之一。手写SQL集中在组合查询、统计报表这些地方就够了没必要到处写XML。数据库用MySQL5.7和8.0都行。要不要上Redis毕设阶段不是必须的但想拉高项目层次后面用Redis缓存药品分类和热点药品属于锦上添花不影响主流程。前端可以用Vue3Element Plus做前后端分离也可以用Thymeleaf做服务端渲染。时间紧张就用Thymeleaf想展示前后端联调能力就选Vue3两种方案都有大量现成项目可参考不存在“选错”的问题。2. 核心业务拆解数据模型与关键逻辑2.1 数据库设计一张药品表远远不够先看药品主表字段设计直接影响后面所有功能。我给的版本是这样的CREATE TABLE drug ( id bigint(20) NOT NULL AUTO_INCREMENT, drug_code varchar(32) NOT NULL COMMENT 药品编码, drug_name varchar(100) NOT NULL COMMENT 药品名称, category_id bigint(20) DEFAULT NULL COMMENT 分类ID, manufacturer_id bigint(20) DEFAULT NULL COMMENT 厂家ID, supplier_id bigint(20) DEFAULT NULL COMMENT 供应商ID, specification varchar(50) DEFAULT NULL COMMENT 规格, unit varchar(10) DEFAULT NULL COMMENT 单位, approval_number varchar(50) DEFAULT NULL COMMENT 批准文号, batch_number varchar(50) DEFAULT NULL COMMENT 生产批号, production_date date DEFAULT NULL COMMENT 生产日期, expiry_date date DEFAULT NULL COMMENT 有效期至, purchase_price decimal(10,2) DEFAULT NULL COMMENT 采购价, sale_price decimal(10,2) DEFAULT NULL COMMENT 售价, stock int(11) DEFAULT 0 COMMENT 当前库存, stock_lower_limit int(11) DEFAULT 0 COMMENT 库存下限, stock_upper_limit int(11) DEFAULT NULL COMMENT 库存上限, status tinyint(1) DEFAULT 1 COMMENT 状态 1上架 0下架, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_drug_code (drug_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT药品信息表;除了drug表整个系统还需要几组配套表drug_category药品分类表、manufacturer厂家表、supplier供应商表这三张属于基础资料表stock_in和stock_in_item是入库单主表和明细表sale_order和sale_order_item是销售单主表和明细表最后再加一张stock_log库存流水表和sys_user用户表。业务拆开成多张表而不是什么东西都放一张大表里数据库设计这一项答辩时就有内容可讲。这里我想特别强调一下库存流水表。每次库存变动入库加库存、销售减库存都要在stock_log里记一条记录包含药品ID、变动类型、变动数量、操作时间。这张表是系统“有据可查”的证明也是后续对账的底表。很多管理系统项目会漏掉这个设计你把它加上项目的完整性立刻上一个台阶。2.2 有效期和库存预警的逻辑到底怎么写两个预警逻辑是整个系统的亮点也是药店业务里最贴实际场景的部分。近效期药品预警就是查“有效期在当前日期N天之内”的药品N一般取90天或者180天。实现上可以直接用SQL条件SELECT * FROM drug WHERE status 1 AND expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 90 DAY);低库存预警更简单查询库存已经小于等于库存下限的药品SELECT * FROM drug WHERE status 1 AND stock stock_lower_limit;关于实现方案有两种选择。一是在查询列表时实时计算也就是上面这种写法优点是数据永远实时缺点是大表时扫描压力稍大但毕设阶段加个索引就够了。二是每天用定时任务扫描生成一张预警记录表适合做消息通知、邮件提醒这类场景但状态会滞后还需要额外维护一套表。对毕设来说我建议用查询时实时算的方式少一张表就少一套同步逻辑代码也更直观。排序字段建议在expiry_date和stock上建索引查询效率没有问题。2.3 分层实现与SpringBoot核心注解使用项目结构按标准的三层来组织Controller负责参数接收和结果返回不写业务逻辑Service负责业务规则和事务控制Mapper负责数据库操作MyBatis-Plus环境下直接继承BaseMapper。SpringBoot的注解体系是整个项目的骨架我整理了一张常用表注解位置作用RestControllerController类返回JSON方法默认带ResponseBodyRequestMapping / GetMapping类或方法映射HTTP路径RequestBody / PathVariable / RequestParam方法参数绑定请求数据ServiceService实现类注册为Spring BeanTransactionalService方法开启数据库事务Mapper / MapperScanMapper接口让MyBatis扫描到MapperTableName / TableId / TableField实体类MyBatis-Plus表映射很多同学天天用SpringBootApplication但不清楚它拆开是什么。它其实是三个注解的组合SpringBootConfiguration标记配置类EnableAutoConfiguration开启自动配置ComponentScan扫描当前包及子包的组件。自动配置的原理核心就是SpringBoot启动时会去加载META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports2.x是spring.factories里的配置类再配合ConditionalOnClass、ConditionalOnMissingBean这些条件注解按需装配。这段话本身就是答辩时“SpringBoot自动配置原理”的标准答法项目做完顺手把这个理清楚比死背八股文管用。3. 实操落地从项目初始化到核心流程跑通3.1 IDEA创建项目与版本选择细节实际创建项目时有几个环节容易卡住我按步骤来说。用IDEA新建工程选择Spring Initializr。网络不好可以换成阿里云镜像地址创建速度会快很多。注意Boot版本下拉框本地JDK是8就选2.7.xJDK 17以上选3.x。如果目录里没有合适的版本或者创建完依赖下载失败基本就是网络源的问题换成阿里云Maven仓库就能解决。依赖勾选时先选Spring Web、MySQL Driver、Validation、Lombok。MyBatis-Plus需要手动引入因为IDEA骨架里没有。pom.xml关键依赖长这样parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这里明确一下2.7.18是SpringBoot 2.x的最后一个维护版本稳定性有保障也是JDK 8环境下的最佳选择。如果选3.xMySQL驱动坐标会变成com.mysql:mysql-connector-j包名也从javax.换成jakarta.很多旧代码就不能直接用了。3.2 核心配置数据源、JSON序列化与分页插件项目跑起来之前先把application.yml写对。这份配置我保留了最关键的几项server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/smart_pharmacy?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: your_password mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8Url里两个参数特别重要。serverTimezoneAsia/Shanghai不加数据库连接会报时区错误或者时间差8小时MySQL 8默认认证插件是caching_sha2_password非SSL连接不加allowPublicKeyRetrievaltrue可能报Public Key Retrieval is not allowed。这两个问题一周能在各种技术群里看到好几遍。MyBatis-Plus的配置里map-underscore-to-camel-case会自动把数据库下划线字段映射成实体类驼峰属性减少大量无意义的代码。分页插件必须单独定义配置类否则Page参数不生效查出来的list还是全量数据Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个坑很多人踩过我一直没理解为什么官方不默认注册分页插件后来想明白了因为要留出给开发者自定义的空间。总之不配这个Bean列表分页就是摆设。3.3 从药品上架到出库扣减的核心代码先看统一返回结构一个项目的规范程度从这里开始public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static T ResultT error(String message) { ResultT r new Result(); r.setCode(500); r.setMessage(message); return r; } }然后看销售出库的核心逻辑这是整个系统技术含量最高的地方。简单粗暴的写法是先查库存判断够不够再执行update。这种做法在并发场景下有严重问题两个用户同时买同一个药都读到了库存为1然后各自扣一次库存变成负数这就是超卖。我推荐用条件更新一次性把“库存充足、药品上架、未过期”三个条件放进SQLService public class SaleServiceImpl implements SaleService { Autowired private SaleOrderMapper saleOrderMapper; Autowired private SaleOrderItemMapper saleOrderItemMapper; Autowired private DrugMapper drugMapper; Autowired private StockLogMapper stockLogMapper; Override Transactional(rollbackFor Exception.class) public Result? createSale(SaleOrderDTO dto) { SaleOrder order new SaleOrder(); order.setOrderNo(SO System.currentTimeMillis()); order.setTotalAmount(dto.getItems().stream() .map(i - i.getSalePrice().multiply(BigDecimal.valueOf(i.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add)); saleOrderMapper.insert(order); for (SaleOrderItemDTO item : dto.getItems()) { int rows drugMapper.deductStock(item.getDrugId(), item.getQuantity()); if (rows 0) { throw new RuntimeException(药品库存不足或已下架: item.getDrugId()); } SaleOrderItem orderItem new SaleOrderItem(); orderItem.setOrderId(order.getId()); orderItem.setDrugId(item.getDrugId()); orderItem.setQuantity(item.getQuantity()); orderItem.setSalePrice(item.getSalePrice()); saleOrderItemMapper.insert(orderItem); StockLog log new StockLog(); log.setDrugId(item.getDrugId()); log.setType(OUT); log.setQuantity(item.getQuantity()); log.setCreateTime(LocalDateTime.now()); stockLogMapper.insert(log); } return Result.ok(order.getId()); } }Mapper里的条件更新语句长这样Update(UPDATE drug SET stock stock - #{num}, update_time NOW() WHERE id #{drugId} AND status 1 AND stock #{num} AND (expiry_date IS NULL OR expiry_date CURDATE())) int deductStock(Param(drugId) Long drugId, Param(num) Integer num);这段代码有个非常实用的设计UPDATE语句自带WHERE条件MySQL的行锁会保证同一时刻只有一个事务能成功更新受影响行数为0就说明库存不足或药品已下架。过期药品也卖不出去SQL条件里直接加了expiry_date CURDATE()。事务加在整个方法上库存扣了一半如果后面出异常会自动回滚不会出现“流水记了但库存没扣”这种数据不一致问题。采购入库就是反向逻辑新增入库单主表和明细表循环update drug表把stock增加再写typeIN的库存流水。业务闭环到这里就能完整串起来了。4. 毕设排错指南SpringBoot项目的常见坑4.1 启动失败和版本错配的处理思路跑项目报错是常态关键是能通过日志快速定位。我把毕设阶段最常见的几个错误整理成了表报错现象常见原因处理方式UnsupportedClassVersionErrorJDK版本低于SpringBoot要求降Boot版本或升级JDKInvalid byte tag in constant pool: 15JDK8跑SpringBoot3改用2.7.x或换JDK17Port 8080 was already in use端口被占用改server.port或杀掉占用进程Failed to configure a DataSource数据源配置缺失检查url、用户名、密码Unknown database数据库没建先执行CREATE DATABASEPublic Key Retrieval is not allowedMySQL8认证插件问题url加allowPublicKeyRetrievaltrue版本问题是头号杀手。SpringBoot 3.x基于Jakarta EE很多包名从javax.改成了jakarta.如果你抄的代码还是老一套一启动就编译不过。想稳定完成毕设我强烈推荐2.7.x加JDK8加MyBatis-Plus 3.5.x这个低风险组合。如果你更想用新特性选3.x加JDK17那就去找Boot3的项目参考别混着抄。4.2 Long精度与时间序列化问题药品表主键如果用了MyBatis-Plus默认的雪花算法它是一个Long类型。传到前端JavaScript之后Number精度不够最后几位会变成0导致根据ID查详情时报错或者查不到。毕设数据量小这个问题不一定能复现但讲给答辩老师听很容易被认为是“系统不够严谨”。解法很简单在ID字段上配置ToStringSerializerBean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder - builder.serializerByType(Long.class, ToStringSerializer.instance) .serializerByType(Long.TYPE, ToStringSerializer.instance); }时间格式是另一个容易出现的问题。LocalDateTime如果不配置格式返给前端是一串数组特别难看。在application.yml里配置jackson的date-format和time-zone能解决大部分场景再配合字段上的JsonFormat(pattern yyyy-MM-dd HH:mm:ss)就比较稳妥了。数据库日期字段用date类型实体类对应LocalDate不要用String去接否则排序、比较都会出问题。4.3 库存并发和事务的坑库存扣减如果写成“先select查库存判断stock 0再update stock stock - 1”两个用户同时买同一个药可能都读到库存为1然后各自扣一次库存变成0甚至负数。毕设并发量小但答辩老师大概率会问“你怎么保证不超卖”这个问题答不上来很伤。正确做法就是前面代码里的条件更新一次性执行UPDATE ... SET stock stock - #{num} WHERE stock #{num}。MySQL的行锁保证同一时刻只有一个事务能更新成功受影响行数为0说明库存不够或者被抢走了。这种写法简洁高效比“查一次锁一次”的方案工程量小得多也更容易讲清楚。事务这块也有三个经典坑。Transactional加在private方法上不生效因为Spring用的是代理机制内部调用不会经过代理方法里catch住异常不往外抛事务不会回滚受检异常默认不回滚所以最好统一写Transactional(rollbackFor Exception.class)。我自己踩过最典型的坑是把入库和扣库存写到一个方法里前半段成功、后半段抛异常因为没加事务库存就多了出来。后来规范了写操作统一进事务方法流水和主表一起成功一起失败问题才彻底解决。5. 答辩准备与项目扩展思路5.1 高频答辩问题与回答切入点给几个必问题目的回答思路都是从真实项目里提炼的比纯背八股文有说服力。第一个为什么选SpringBoot答快速集成、自动配置、内嵌容器、生态成熟。具体到项目里不需要单独部署Tomcat一个java -jar就能跑起来开发效率高。第二个SpringBoot自动配置原理答核心是EnableAutoConfiguration启动时加载AutoConfiguration.imports或spring.factories中的配置类通过ConditionalOnClass、ConditionalOnMissingBean等条件注解按需装配。第三个SpringBoot启动流程大概是什么答创建SpringApplication推断应用类型加载Initializer和Listener准备Environment创建容器刷新容器执行runner。答到刷新容器和自动装配两层就够用。第四个库存扣减如何防超卖答条件更新加事务SQL里直接写stock #{num}必要时再加乐观锁version字段或Redis预扣库存。第五个有效期预警怎么设计答查询时用SQL条件expiry_date DATE_ADD(CURDATE(), INTERVAL 90 DAY)并在expiry_date上加索引。第六个药品入库和销售为什么要写库存流水答保证数据可追溯是库存对账的依据也是药品流通管理的基本要求。第七个如果做成前后端分离跨域怎么处理答后端配置CorsFilter或CrossOrigin前端也可以用代理转发。这些回答不一定每个字都标准但每一个都能落到自己的项目代码上老师一听就知道你是真做过不是背出来的。5.2 可以快速加分的扩展点主流程已经很完整了想冲刺更高分可以从这几个方向扩展。Redis缓存。用SpringBoot整合Redis缓存药品分类和首页热点数据减少数据库压力。配置一个RedisConfig用StringRedisTemplate操作字符串代码量不大但能讲出一个完整的缓存方案。这是“Redis在SpringBoot中的使用”最典型的落地场景。消息队列。下单成功后发送MQ消息触发补货提醒或者异步写操作日志。比如SpringBoot整合ActiveMQ或RabbitMQ引入一个简单队列把销售事件丢进去。时间不够就只写设计思路不一定要真部署一套Broker。多数据源。把系统操作日志放到另一个MySQL库用MyBatis-Plus的多数据源插件DS切换数据源。这个属于架构层面的加分项很多评分表都会给这类设计加分。前端升级成Vue3Element Plus。把后端接口按REST风格规范化前端单独工程跑起来配合JWT做登录态就是标准的前后端分离项目形态。这一套组合在市场上非常常见做完以后写进简历也有内容可说。扩展的原则很明确不要影响主流程的稳定性先跑通再装饰否则答辩现场真机演示时出了bug反而得不偿失。5.3 一点真实体会这套系统我在不同阶段看感受完全不一样。第一次写的时候我也是图省事一张drug表把所有字段堆完CRUD写完就以为结束了。后来重新设计加了入库单、销售单、库存流水才发现系统的骨架一下子清晰了——不是表越多越好而是每一张表的职责要清楚业务闭环要完整。如果让我给一个最直接的建议就是把“药品档案—采购入库—销售出库—库存预警—库存流水”这几个环节想明白再动手写代码。数据库表设计好了后面代码写起来非常顺表设计随意后面每一步都在填坑。这个项目做完以后我顺手把SpringBoot的自动配置、事务、缓存这些点单独整理了一遍后面面试被问到类似问题都能从项目里拿出真实场景来回答这一点比单纯刷题有价值得多。哪怕你目前的目标只是顺利毕业这套“从业务出发反推技术方案”的思路学下来也不亏。