ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot药店药品管理系统设计与实现:从进销存到效期预警

SpringBoot药店药品管理系统设计与实现:从进销存到效期预警 干了几年Java开发带过不少应届生也看过大量计算机毕业设计的开题报告。说实话药店药品管理系统这个题目几乎年年都有但大部分人都写在表面做几个CRUD页面、搭个简单的进销存好不容易拼出来的系统答辩时一问业务逻辑就垮掉。原因很简单大家把这个题目当成普通的增删改查练习忽略了它在医药流通场景下的真实约束。如果你正在准备SpringBoot方向的毕业设计或者想做一个真正能落地的WEB管理平台药店药品管理系统是一个非常典型且性价比极高的选题。它覆盖了用户认证、权限管理、进销存核心流程、库存预警、销售统计等一系列高频技术点而且业务规则清晰——药品有批号、有有效期、有GSP合规要求比单纯的学生管理系统或图书管理系统有更多可深挖的空间设计得好完全可以成为作品集中的亮点。这篇内容我会按自己的实操经验把这个系统的设计与实现从头拆一遍包括技术选型逻辑、数据库设计、核心模块编码思路、容易踩的坑以及答辩时可能会被问到的问题。全程基于SpringBoot B/S架构展开适合真正想动手做出来、而不只是停留在PPT层面的同学参考。1. 项目整体设计与思路拆解1.1 先搞清楚B/S架构为什么是标配选择B/S架构即浏览器/服务器架构用户通过浏览器访问系统无需安装任何客户端。选择B/S架构做药店管理系统核心原因只有一个药店门店的数量和位置是分布式的而浏览器是每台电脑都具备的通用入口。举个例子一个社区药房可能有1个总部加3家分店管理人员需要在总部查看各门店的库存和销售数据门店店员只需要在本店操作收银和入库。如果是C/S架构每个门店都要安装客户端版本更新时要逐一升级而B/S架构下只需要部署一套服务端所有人在浏览器里访问同一个地址即可。从毕业设计的角度B/S架构也是更合适的选择它天然契合SpringBoot的RESTful API设计风格前后端通过JSON交互逻辑清晰答辩时容易讲清楚。1.2 SpringBoot框架的技术优势与选型理由SpringBoot不是新技术但它依然是当前Java后端开发的事实标准。为什么不用SSHStruts2 Spring Hibernate或者SSMSpring SpringMVC MyBatisSSM虽然也能做但配置繁琐光是Spring和SpringMVC的XML配置就能写几十行。SpringBoot把这些配置全部自动化内置Tomcat一个mvn spring-boot:run就能启动项目开发效率提升一个量级。从就业和实用角度看SpringBoot是市场主流掌握它是基本要求。而且SpringBoot生态完整Spring Security、Spring Data JPA、MyBatis-Plus、Redis等可以无缝集成这些都是真实项目中常用的技术栈。对于药店管理系统来说涉及的模块不算特别复杂SpringBoot的轻量特性刚好够用不会像微服务架构那样引入不必要的复杂度。1.3 医药门店场景下的核心业务需求解读在设计系统之前必须先理解药店的业务场景。社区药房的日常运营可以拆成几个核心环节采购入库店长根据库存情况向供应商下采购单药品到货后验收入库记录批号、生产日期、有效期、采购价。销售出库店员收银时选择药品系统扣减库存生成销售记录记录销售价和数量。库存管理查询各药品的实时库存、效期状态、滞销或畅销情况库存下限预警。供应商管理管理供应商档案跟踪采购记录结账周期等。系统管理用户管理、角色权限分配、操作日志。这些环节中最关键的是批号有效期管理。普通商品库存按SKU维度统计即可但药品必须精确到批次。同一款药品不同批次的进货价可能不同有效期也不同销售时必须遵循近效期先出原则这就对数据表设计和库存扣减逻辑提出了更高的要求。能在设计里体现这一点你的系统就比绝大多数毕设高出一个档次。2. 数据库设计与核心表结构解析2.1 药品信息建模基础信息码表是关键药品主表是所有业务数据的核心字段设计直接影响后续开发效率。建议采用以下思路药品基础信息表drug_info至少要包含药品编号、通用名、商品名、剂型、规格、生产厂家、批准文号、单位、零售价、会员价、库存下限、分类ID。其中药品编号建议使用自定义编码规则比如前两位代表分类中间四位代表厂家后四位为流水号。之所以不直接使用国药准字作为主键是因为国药准字只关联到批准文号级别同一批准文号下可能存在多个规格无法唯一标识一个具体药品。分类建议单独建表drug_category通过parent_id字段支持多级分类父分类为处方药、非处方药、保健品子分类再细分。这样前台可以通过分类树逐级筛选后台统计时也可以按大类汇总。2.2 进销存核心表关系设计进销存数据流的核心是三张单据表采购入库单、销售单、库存表再加上对应的明细表。采购入库单purchase_order单号、供应商ID、入库总金额、入库数量、操作人、入库时间、备注。采购入库明细purchase_order_item关联入库单ID、药品ID、批号、生产日期、有效期、采购价、数量。销售单sale_order单号、会员ID可空、应收金额、实收金额、操作人、销售时间。销售明细sale_order_item关联销售单ID、药品ID、批号、销售单价、数量、小计。库存表drug_stock药品ID、批号、生产日期、有效期、库存数量、门店ID。其中最关键的设计是库存表的主键不是药品ID而是药品ID 批号 门店ID的联合唯一键。这样一来同一药品不同批次的入库记录在库存表中各自独立销售时才能按批号精确扣减也才能实现效期预警和近效期先出。举一个实际场景阿莫西林胶囊6月1日入库400盒批号A6月15日入库300盒批号B。库存中这两条记录分开存储销售扣减时优先扣批号A效期更早。如果库存只有药品维度这批逻辑根本无法实现。2.3 效期管理和库存预警的字段设计效期管理是药店系统区别于普通进销存系统的重要特征。GSP规定近效期药品有效期不足6个月需要重点监控过期药品必须停售并报损。为了实现效期预警不需要额外建表通过SQL条件查询即可SELECT drug_id, batch_no, expire_date, quantity FROM drug_stock WHERE expire_date BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL 6 MONTH) ORDER BY expire_date ASC;库存下限预警则依赖药品主表中的stock_lower_limit字段查询时比较当前总库存量与下限值SELECT d.drug_name, d.stock_lower_limit, COALESCE(SUM(s.quantity), 0) AS total_stock FROM drug_info d LEFT JOIN drug_stock s ON d.id s.drug_id GROUP BY d.id HAVING total_stock d.stock_lower_limit;这两条SQL是系统里最核心的查询逻辑建议把查询结果封装成独立的VO对象返回给前端分别展示在效期预警和库存预警两个页面上。3. 核心功能模块实现与关键代码走读3.1 用户认证与权限控制的工程实践药店管理系统涉及三类角色管理员老板、店长采购管理、店员销售收银。权限设计采用经典的RBAC模型——用户表sys_user、角色表sys_role、菜单表sys_menu、用户角色关联表、角色菜单关联表。五张表不多不少刚好覆盖需求。认证方案建议使用JWT而非传统的Session。原因有两点一是前后端分离开发模式下JWT无状态的特征让接口鉴权更加简单二是毕业设计答辩时讲清楚JWT的token生成、解析、过期刷新机制是一个明显的加分项。核心思路是用户登录成功后服务端生成一个包含用户ID和角色信息的token返回给前端前端存储在localStorage中之后每次请求在请求头携带Authorization: Bearer token后端通过拦截器统一校验Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/login)) { return true; } String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } response.setStatus(401); return false; } }角色权限则通过自定义注解RequireRole(ADMIN)加在Controller方法上在拦截器中判断当前用户是否拥有对应角色实现接口级别的权限管理。3.2 药品入库流程的完整实现思路药品入库是药店系统中数据准确性要求最高的流程。一个完整的入库操作在事务中要完成以下步骤校验供应商是否存在且状态正常。校验入库单明细中每一项药品是否存在。计算入库单总金额生成入库单号和明细记录。更新库存判断药品ID 批号 门店ID的记录是否已存在。存在则累加库存数量。不存在则新建库存记录。记录操作日志。这里最容易出错的是第4步。因为同一个药品的同一个批次可能分多次入库正常做法是先查询库存表存在则更新数量不存在则插入。但在高并发场景下这种先查后改的操作会产生竞态条件。解决办法是给库存表加上唯一索引drug_id, batch_no, store_id然后在插入时捕获DuplicateKeyException捕获到就改为更新操作或者直接用INSERT ... ON DUPLICATE KEY UPDATEINSERT INTO drug_stock (drug_id, batch_no, produce_date, expire_date, quantity, store_id) VALUES (#{drugId}, #{batchNo}, #{produceDate}, #{expireDate}, #{quantity}, #{storeId}) ON DUPLICATE KEY UPDATE quantity quantity VALUES(quantity);这条SQL是保证库存数据准确的关键也是面试中常问的如何避免重复插入导致的数据不一致问题的答案。3.3 销售出库事务边界与库存扣减时机销售收银的核心是一次销售可能包含多款药品每款药品又可能从不同批次扣减。实现思路上销售服务方法必须加Transactional执行以下步骤遍历销售明细逐项查询库存。检查库存是否充足不足则抛出异常回滚整个事务。按效期从近到远扣减库存先扣最早到期的批次。计算总金额生成销售单和销售明细。扣减库存数量如果某批次的库存数量减到0保留记录即可不建议物理删除为了保留历史追溯。扣减库存时需要注意一个实际问题A药品有批号X剩余5盒、批号Y剩余10盒本次销售8盒。正确扣减结果是X批次扣5盒变为0Y批次扣3盒变为7盒。实现时可以用一个循环来处理for (SaleOrderItem item : saleItems) { int needQuantity item.getQuantity(); // 按有效期升序查询可用库存 ListDrugStock stocks stockMapper.selectAvailableStock(item.getDrugId(), item.getStoreId()); for (DrugStock stock : stocks) { if (needQuantity 0) break; int available stock.getQuantity(); int deduct Math.min(available, needQuantity); stockMapper.deductQuantity(stock.getId(), deduct); needQuantity - deduct; } if (needQuantity 0) { throw new BusinessException(药品[ item.getDrugName() ]库存不足); } }这里有个隐藏问题值得思考如果在扣减过程中第5个药品发现库存不足整个事务会回滚前4个药品的库存扣减操作会一并撤销。这就是事务的意义所在也是我在实际开发中特别强调的——一定不要把扣库存的逻辑分散到多个独立事务中执行否则会出现库存扣减与销售单据对不上的问题。3.4 近效期先出的业务规则实现近效期先出FEFOFirst Expire First Out是医药行业区别于普通零售行业的特有规则指销售时应优先出售最快到达有效期的批次。这个规则不需要单独写一个调度任务而是在销售扣减库存时天然实现。因为查询可用库存的SQL已经按expire_date ASC排序销售优先扣最早到期的批次。但还有一个补充规则要处理过期药品不能销售。查询可用库存时必须过滤掉已经过期的批次SELECT id, drug_id, batch_no, quantity, expire_date FROM drug_stock WHERE drug_id #{drugId} AND store_id #{storeId} AND expire_date NOW() AND quantity 0 ORDER BY expire_date ASC;加一个expire_date NOW()条件从源头掐断过期药品售出的可能。这是整个销售模块中最重要的一个条件也是答辩时能体现你业务理解深度的细节。4. 前端实现与接口设计规范4.1 页面架构与组件拆分思路前端推荐使用Vue 3 Element Plus通过axios调用后端接口。整体布局采用左侧菜单、右侧内容区的方式菜单根据登录用户的角色权限动态生成。页面拆分的核心思路是一模块一页面建议包含登录页主布局页包含顶栏、侧边栏、面包屑、标签页药品管理页列表、新增、编辑、详情弹窗分类管理页供应商管理页采购入库页选择药品、填写批号效期、生成入库单入库记录页收银台页面扫描药品条码/输入药品名快速检索、购物车式结算销售记录页库存查询页展示各批次库存、效期预警页面效期预警 库存预警放在同一页面Tab切换统计分析页按日/周/月展现销售趋势、畅销药品排行用户管理页面管理员专属角色权限管理页面建议把收银台做成单独的全屏页面模拟真实的收银终端体验。药品检索要支持输入拼音码如阿莫西林的拼音首字母AMXL快速定位这个功能依赖数据表中增加一个pinyin_code字段可以在入库时自动生成。4.2 统一响应体与异常处理规范前后端分离开发时接口返回数据的结构如果不统一前端处理逻辑会非常痛苦。强烈建议定义统一的返回结果类Data public class ResultT { private Integer code; // 200成功其他为失败 private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.setCode(200); r.setData(data); return r; } public static T ResultT error(String message) { ResultT r new Result(); r.setCode(500); r.setMessage(message); return r; } }同时要配合全局异常处理器把业务异常、参数校验异常、未知异常统一转换为Result结构返回。这一套组合做下来前端axios拦截器只需要判断response.data.code即可决定是弹错误提示还是正常渲染。实际开发中前端常因为字段名不对导致数据渲染不出来。建议在接口设计阶段就列好字段名清单后端返回的JSON字段统一使用驼峰命名前端直接使用不做二次字段映射。4.3 前端药品条码快速录入实现真实的药店收银系统扫码枪是最常用的录入方式。扫码枪的本质是一个键盘输入设备它会把扫描到的条码快速连续输入到当前聚焦的输入框中然后自动触发回车事件。前端实现时只需要一个隐藏的输入框接收条码录入监听回车事件后向后端发起查询handleBarcodeInput() { if (this.barcodeInput.length 6) return; if (event.key Enter || event.code Enter) { this.addItemByBarcode(this.barcodeInput.trim()); this.barcodeInput ; } }后端接口根据药品条码国药准字号或自定义条码查询药品信息返回后添加到购物车列表中。整个交互流程流畅的话演示效果会非常出彩因为很多同学的收银模块只能靠手输药品名整体体验差很多。5. 部署环境搭建与项目上线实操5.1 本地开发环境准备在开始编码之前先把开发环境准备好这是最基础也最容易出问题的一环。推荐版本组合是JDK 1.8SpringBoot 2.7.x 完全兼容Maven 3.6MySQL 5.7 或 8.0Redis可选用于缓存验证码和token黑名单IDEA 2022 以上版本SpringBoot版本建议选择2.7.18这是2.x系列最后一个稳定版本。不要为了追新而选择SpringBoot 3.x因为它基于JDK 17对JDK 1.8环境不友好而且部分第三方依赖的兼容性问题排查起来非常耗时。如果学校机房环境只有JDK 1.8用SpringBoot 2.7.18是最稳妥的选择。依赖方面核心就这几个spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java、jjwtJWT、lombok、hutool。Hutool是一个非常实用的Java工具库里面的日期工具、字符串工具、验证码生成器能省下大量重复代码。5.2 项目打包与部署流程项目开发完成后打包部署是必须走通的一环。SpringBoot项目使用Maven打包mvn clean package -DskipTests打包完成后在target目录下会生成一个可执行的jar文件。使用以下命令启动java -jar drug-store-system.jar --spring.profiles.activeprod如果是部署到服务器推荐使用systemd配置守护进程这样即使服务器重启服务也能自动启动。配置文件建议拆分# application-dev.yml 本地开发 spring.datasource.urljdbc:mysql://localhost:3306/drug_store?useUnicodetruecharacterEncodingutf8 spring.datasource.usernameroot spring.datasource.password123456 # application-prod.yml 生产环境 spring.datasource.urljdbc:mysql://你的服务器IP:3306/drug_store?useUnicodetruecharacterEncodingutf8 spring.datasource.usernamedrug_user spring.datasource.password复杂密码部署时重点留意MySQL的时区配置如果连接串不加serverTimezoneAsia/Shanghai日期类型的字段查询出来可能比实际时间少8个小时。这个坑出现的频率极高一度让我排查了很久。5.3 初始化数据的准备策略数据库表结构设计完成后需要准备一些初始化数据让系统一启动就能演示。建议准备以下几类管理员账号admin/admin123店长账号manager/123456店员账号clerk/12345610个左右药品分类抗生素类、感冒类、消化系统类、心脑血管类、维生素类等30~50种常用药品每款药品至少2个批次方便演示效期预警5个供应商近一个月的销售记录通过SQL脚本生成用于统计页展示初始化数据的导入方式推荐在项目启动时通过CommandLineRunner自动判断如果用户表为空则自动写入初始数据。这样换一台电脑导入项目后不用手动执行SQL文件直接启动就能看到完整页面。6. 常见问题与排查技巧实录6.1 MyBatis-Plus分页查询失效的经典原因MyBatis-Plus的分页功能需要单独配置分页插件很多同学在写代码时直接调用selectPage方法结果发现分页条件完全不生效返回的还是全量数据。原因就是少了分页插件的配置。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }另一个容易踩的坑是当查询条件中包含自定义SQL或者多表联查时分页插件有时无法自动解析count语句。解决方案是在自定义Mapper方法中手动提供分页后的查询SQL或者用MyBatis-Plus提供的Wrapper来构造条件避免直接写复杂的联表SQL。6.2 金额精度丢失问题Float和Double不能用于金额Java中float和double都存在精度问题比如0.1 0.2的结果是0.30000000000000004。药品销售涉及真金白银的金额计算精度丢失是绝对不可接受的。金额字段统一使用BigDecimal类型数据库中使用DECIMAL(10, 2)。在Java代码中所有金额运算必须使用BigDecimal的方法BigDecimal total new BigDecimal(0.00); for (SaleOrderItemVO item : items) { BigDecimal subtotal item.getPrice().multiply(new BigDecimal(item.getQuantity())); total total.add(subtotal); }注意BigDecimal构造函数一定要传入String类型不能直接传入doublenew BigDecimal(0.1)的结果是0.1000000000000000055511151231257827一样有精度问题。这个知识点在答辩时被问到的概率非常高务必理解清楚。6.3 跨域访问被拦截的报错处理前后端分离开发时前端运行在http://localhost:5173后端运行在http://localhost:8080端口不同即为跨域。如果不做处理浏览器会直接拦截请求。解决方式有两种。一种是在后端添加全局CORS配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }另一种是使用前端代理在Vue项目的vite.config.js中配置proxy将/api前缀的请求代理到http://localhost:8080。推荐使用第二种方式因为生产部署时前后端同源代理配置可以保留而如果使用全局CORS上线时必须记得调整否则会留下安全隐患。6.4 药品重复入库导致库存翻倍的坑这个问题的根源在于入库操作时没有以药品 批号为唯一性依据做幂等控制。比如网络卡顿导致用户多次点击确认入库按钮后端会收到多个相同的请求库存就会翻倍增加。前端可以做按钮防重复提交点击后置灰但这只能解决部分问题真正的保证要落在后端。方案是入库单号由后端生成并且带幂等标识前端先调用接口获取入库单号再携带该单号提交入库请求后端对入库单号建立唯一索引重复提交时直接抛出入库单已存在的异常。这套机制在多门店协同或者高并发场景下尤为重要。7. 系统测试与答辩准备要点7.1 功能测试用例设计思路系统开发完成后建议整理一份测试用例文档一方面在答辩时可以展示你具备基本的测试意识另一方面也能帮助自己发现隐藏的bug。核心流程的测试用例至少需要覆盖以下场景正确账号密码登录成功错误密码登录失败。不同角色登录后可见菜单不同。药品新增必填项为空时提示错误正常填写后保存成功。药品入库正常入库后库存增加再次入库同批次库存累加。药品入库填写已过期效期时系统给出提示或拒绝入库。销售收银购物车添加多款药品计算总金额正确。销售收银库存不足时给出提示且不生成销售单。库存预警将某种药品库存手动调至低于下限预警页面能查询到。效期预警查询到6个月内到期的批次记录。不同角色访问越权接口时后端拦截并返回401。7.2 答辩时的项目讲解主线梳理答辩时长通常为10到15分钟时间有限不建议按页面功能平铺直叙地讲而是按照业务背景 - 技术方案 - 核心难点 - 演示亮点这条主线来讲。业务背景部分说明社区药房在实际经营中遇到的痛点是库存管理混乱、效期管理缺失、销售数据不透明这个系统就是为了解决这些问题。技术方案部分重点讲清楚SpringBoot Vue的总体架构B/S架构的选型理由JWT认证机制数据库设计中的批号维度库存管理思想。核心难点部分把近效期先出规则、严格的事务控制、库存幂等扣减这三点讲清楚每个点都需要配合代码或SQL来解释这部分是拉开差距的地方。演示亮点部分演示时可以着重展示两个场景。第一个是近效期先出——先让评委看到当前库存中某药品有两个批次然后演示一次销售操作让评委观察到最早到期的批次被优先扣减。第二个是库存预警——把某药品的库存调整到低于下限刷新预警页面后看到对应的提示。8. 写在最后的一些实操心得整个药店药品管理系统做下来我最深刻的体会是毕业设计的差距不取决于技术用得多新、功能堆得多全而取决于你对业务逻辑的理解深度。同样是SpringBoot进销存系统能够讲清楚批号和效期管理的人和只做了基础CRUD的人最终呈现的效果天差地别。如果你时间有限我建议的优先级是先保证核心流程跑通入库和销售闭环再补齐权限管理最后再做统计报表。前面的模块是系统的骨架后面的模块是加分项。不要一上来就纠结UI细节把业务流程做扎实了页面自然会好看。最后再分享一个小技巧项目里要准备好一个一键启动的说明文档写清楚JDK、MySQL版本、如何导入SQL脚本、如何启动前后端。答辩现场经常遇到电脑环境不一致导致项目启动不了的情况这份文档不仅能帮你快速恢复环境给评委看的时候也能留下做事规范的印象。
返回列表