社区药店系统Java毕设:从数据库设计到库存扣减的完整实战

社区药店系统Java毕设:从数据库设计到库存扣减的完整实战 做Java毕业设计“社区药店系统”是个出现率极高的题目。但说实话同样一个题目有人能做出一个能过答辩、能写进简历、甚至敢在面试时拿出来讲的完整项目有人做完就只是一个“能跑起来的CRUD”。差别不在题目本身而在于你怎么拆需求、怎么设计表、怎么处理库存和销售这几条核心链路。这篇文章不打算只讲“怎么把项目跑起来”而是从选型到表结构、从核心代码到排查思路把社区药店系统该有的东西完整过一遍。无论你是正在选题的应届生还是想快速上手SpringBoot做管理系统的初级开发者这篇都能当一份实操参考。1. 项目定位与核心需求拆解1.1 从毕设题目的字缝里先抠清楚业务边界很多同学拿到题目就开始写代码这是最容易翻车的做法。你先冷静下来看看这个标题“社区药店智能管理平台”“药品销售与库存管理系统”。关键词是“社区”“销售”“库存”不是“大型连锁ERP”更不是“医药电商中台”。社区药店的业务其实很清晰药品从供应商进货入库到门店库存顾客来买药时开单收款库存同步扣减。遇到近效期药品要提醒库存不够了要预警。就是这么一条线。你需要覆盖的无非是几个核心模块药品信息管理药品名称、通用名、规格、生产厂家、批准文号、剂型、售价、进价。库存管理入库、出库、盘点、库存查询、近效期预警、库存上下限预警。销售管理收银开单、销售明细、退货、日结统计。供应商管理供应商档案、采购记录。系统管理用户登录、角色权限、操作日志。有了这条主线你再去给前台收银、库管员、店长/管理员各分一套界面和权限这个项目就不会散。我见过很多失败的毕设问题几乎都一样页面倒是写了一大堆结果业务是碎的销售不扣库存入库不产生批次记录退货更是完全没有。1.2 技术选型为什么这套组合最适合毕业设计这个题目自带SpringBoot这基本就是现在Java后端项目的标配。我的建议是不要自己再加戏不要在毕设里硬塞微服务、消息队列、分布式事务这些。原因不只是能力问题而是答辩的时候老师会问你“为什么用这个”你说不清楚就是给自己挖坑。合理的组合应该是后端SpringBoot 2.7.x MyBatis-Plus MySQL 8.x安全认证JWT Spring Boot拦截器或者更简单一点用Sa-Token也很方便前端Vue 3 Element-Plus或者直接用Thymeleaf做服务端渲染缓存Redis集中管理字典数据、药品分类缓存SpringBoot 2.7.x是目前最稳的一个版本线网上资料最全遇到问题基本都能搜到答案而且不会像SpringBoot 3那样把javax换成jakarta很多旧教程里的代码直接不能用。MyBatis-Plus这层是帮了大忙的分页插件一套就是好东西别手写PageHelperLambdaQueryWrapper写起来比XML舒服太多了。可能有同学纠结前端要不要单独做我的意见是如果你时间够就做成前后端分离Vue3 Element-Plus一套下来界面好看答辩的时候加分明显。如果时间紧或者前端不熟用Thymeleaf Bootstrap也不丢人重要的是后端逻辑能不能讲清楚。1.3 角色权限与核心流程设计这套系统里用户角色不是摆设你设计的时候就要把权限边界想清楚管理员查看统计报表、管理药品字典、管理用户、审核采购。库管员负责供应商管理、药品入库、库存盘点、近效期处理。收银员销售开单、退货、查询药品、查库存。这样的角色划分会直接影响你的数据库设计和接口权限控制。具体的实现方式后面专门讲。再梳理一下核心流程采购入库流程采购员/库管员选择供应商和药品生成采购单确认收货后药品入库库存增加同时生成入库批次记录带生产日期、有效期、批号。销售出库流程收银员选择药品加入购物车结算时系统校验库存并扣减生成销售主单和销售明细。这里有个关键点药品要按批次先进先出FIFO扣减库存而不是单纯地把总库存减一。退货流程根据销售单号找出原销售记录回补库存记录退货原因。这三条线理顺了你的系统就成功了一大半后面只是把CRUD落到代码里而已。2. 数据库设计药品销售与库存管理的表结构2.1 药品信息与批次管理数据库设计是这个项目真正的分水岭。我审过不少类似的毕设很多人一上来就是一张drug表放了药名、数量、进价、售价然后所有逻辑都在操作这张表。这种设计放到十万人的小区药店勉强能“糊”过去但放到答辩上老师问几个问题你大概率圆不回来。正确的做法是把药品主数据和批次库存拆开drug药品信息表id主键drug_code药品编码系统生成或手动维护drug_name通用名这个很重要因为药店的人习惯用商品名但统计的时候按通用名聚合才有意义spec规格比如“0.25g*24片/盒”manufacturer生产厂家approval_number批准文号比如“国药准字HXXXXXXXX”unit基本单位盒、瓶、支sale_price销售价注意区分含税不含税purchase_price采购价prescription_type处方药还是OTC非处方药处方药销售在合规上要求更高stock_lower_limit库存预警下限status上下架状态drug_batch药品批次表也是库存表iddrug_id关联药品batch_no生产批号同一药品每批货这个不同production_date生产日期expire_date有效期至quantity当前库存数量supplier_id供货来源为什么要单独一张批次表因为药品是强效期管理商品药店最怕的就是药品放在货架上过了期还不知情。批号和有效期挂在销售明细里真出了效期投诉你能查到这批药是从哪个供应商哪一批进来的这是硬需求。同时FIFO出库扣减库存的时候批次表也能派上大用场后面详细说。2.2 销售订单与库存扣减逻辑销售这层最少需要两张表销售主表和销售明细表标准的一对多设计。sale_order销售主表idorder_no销售单号格式建议按日期生成比如202406011530001user_id收银员member_id可空如果有会员体系就是会员IDtotal_amount商品金额汇总discount_amount优惠金额payable_amount应收金额actual_amount实收金额change_amount找零sale_time销售时间refund_status是否部分/全部退货sale_order_item销售明细表idorder_id关联销售主表drug_iddrug_batch_id关联到具体批次这是FIFO扣减留下的痕迹quantitypriceamount这里有个非常容易被忽略的问题销售明细里存的price不能是drug表里的当前售价而应该是成交时这一单的实际单价。因为商品价格是会调整的你不可能半年后统计的时候发现每天的销售单价全部变成了最新价格。明细表存快照这是一个关系型数据库建模里最基本的习惯。2.3 供应商与入库单设计supplier供应商表idsupplier_namecontact_personcontact_phoneaddressstatuspurchase_order入库单/采购单idpurchase_nosupplier_iduser_id经手人total_amountstatus待入库、已入库、已作废create_timeconfirm_timepurchase_order_item入库明细idpurchase_iddrug_idbatch_noproduction_dateexpire_datequantitypurchase_priceamount入库的时候把批次信息写进明细确认入库时除了写purchase_order的status还要把批次信息同步到drug_batch表里增加库存。这个过程务必要放在一个事务里否则很容易出现“入库单已确认但库存没有加上”或者“库存加了入库单还是待入库”这种错误状态。其余像user表、role表、menu表这几张通用表就不展开说了标准的RBAC基于角色的访问控制设计网上例子一抓一大把。把上面四组核心表建对这个项目的数据根基就稳了。3. 后端核心功能落地与关键代码3.1 SpringBoot 项目初始化与分层结构很多人卡在第一步创建项目。如果你用的是IDEA新建SpringBoot项目的时候经常遇到start.spring.io连不上导致卡住的情况解决方案其实很简单把Server URL换成阿里云的镜像地址https://start.aliyun.com。不要从官网下载zip再导进来直接换镜像源创建速度会快很多也省去一堆导入依赖时的等待时间。包结构我这里给一个经过验证的分层方式分包尽量简单清晰com.pharmacy ├── common # 通用工具、统一返回结果、异常处理 ├── config # 配置类MyBatis-Plus分页、拦截器、跨域 ├── controller # Controller层 ├── service # Service层先写接口再写实现 ├── mapper # MyBatis-Plus的Mapper接口 ├── entity # 实体类 ├── dto # 入参出参对象 └── vo # 视图对象给前端组装数据不少同学写项目喜欢把Controller里的东西堆一大堆逻辑这是大忌。Controller只做参数接收和结果返回业务逻辑一律放到Service层。这样写的直接好处是你的Service层方法能被多个Controller和别的方法复用而且以后写单元测试也能省不少力。要是时间充裕在写Service的时候顺手把接口类定义出来这算是一个非常好的职业习惯。3.2 库存扣减的核心实现这个模块是社区药店系统的灵魂做得好不好答辩时经不经得起追问就看这里了。先明确一个基本策略销售出库按FIFO先进先出扣减。什么意思同一款药品3月进货的那批有效期可能比5月进货的那批早到期你出库就应该先卖3月那批。这么做既能保证效期管理不出问题也符合实际药店的经营习惯。具体流程收银台提交购物车数据ListDrugId, Quantity。打开事务。对每一个条目按“有效期从近到远”查询该药品的可用批次。逐批次扣减库存直到满足购买数量。生成销售主单和销售明细明细里记录对应批次ID。提交事务。写成代码大概是这个思路Service public class SaleServiceImpl implements SaleService { Autowired private DrugBatchMapper drugBatchMapper; Autowired private SaleOrderMapper saleOrderMapper; Autowired private SaleOrderItemMapper saleOrderItemMapper; Override Transactional(rollbackFor Exception.class) public SaleOrderVO createOrder(SaleRequest request) { // 1. 先生成销售主单状态先置为待结算或者直接生成好再写明细 SaleOrder order new SaleOrder(); order.setOrderNo(SALE System.currentTimeMillis()); order.setUserId(request.getUserId()); order.setSaleTime(LocalDateTime.now()); order.setRefundStatus(0); saleOrderMapper.insert(order); BigDecimal totalAmount BigDecimal.ZERO; // 2. 遍历购物车明细 for (SaleItemRequest item : request.getItems()) { int remain item.getQuantity(); // 3. 查询当前药品的批次列表按有效期升序FIFO ListDrugBatch batches drugBatchMapper.selectBatchesByDrugIdOrderByExpire( item.getDrugId()); for (DrugBatch batch : batches) { if (remain 0) { break; } int deduct Math.min(batch.getQuantity(), remain); // 4. 乐观锁扣减当前批次库存 int rows drugBatchMapper.deductStock(batch.getId(), deduct, batch.getQuantity()); if (rows 0) { throw new ServiceException(库存不足或已被并发修改); } // 5. 写销售明细记录批次ID和成交价格 SaleOrderItem orderItem new SaleOrderItem(); orderItem.setOrderId(order.getId()); orderItem.setDrugId(item.getDrugId()); orderItem.setDrugBatchId(batch.getId()); orderItem.setQuantity(deduct); orderItem.setPrice(batch.getSalePrice()); orderItem.setAmount(batch.getSalePrice().multiply( BigDecimal.valueOf(deduct))); saleOrderItemMapper.insert(orderItem); remain - deduct; } if (remain 0) { throw new ServiceException(药品库存不足 item.getDrugId()); } } return buildOrderVO(order.getId()); } }对应的SQL用MyBatis-Plus的UpdateWrapper也能写但为了清晰我更建议直接在Mapper里手写一条注解SQLUpdate(UPDATE drug_batch SET quantity quantity - #{deduct} WHERE id #{batchId} AND quantity #{deduct}) int deductStock(Param(batchId) Long batchId, Param(deduct) Integer deduct);这条SQL是关键中的关键。注意最后的“quantity #{deduct}”这个条件它是整个高并发安全的核心。也就是说如果同一时刻有两个收银员都在卖同一批药数据库的行锁会保证只有一个更新成功另一个会因为条件不满足而更新0行然后你在代码里抛出异常整个事务回滚。这个方案比单纯的“先select判断库存够不够再update”要安全得多因为它把“判断”和“扣减”合并成了一个原子操作。3.3 销售统计报表的实现报表是管理员的“仪表盘”也是很多老师喜欢问的功能。你要做的不是简单地select sum一下而是要按时间段、按药品维度去统计方便店长看清每天卖了什么、毛利多少。建议在service层实现这样一个核心方法按日期分组统计销售额、订单量、毛利。public ListDailySaleStatDTO getDailySaleStat(String startDate, String endDate) { return saleOrderMapper.selectDailyStat(startDate, endDate); }对应的SQL其实写起来很简单SELECT DATE_FORMAT(sale_time, %Y-%m-%d) AS stat_date, COUNT(*) AS order_count, SUM(payable_amount) AS sale_amount, SUM(payable_amount - total_amount) AS gross_profit FROM sale_order WHERE sale_time #{startDate} AND sale_time DATE_ADD(#{endDate}, INTERVAL 1 DAY) GROUP BY DATE_FORMAT(sale_time, %Y-%m-%d) ORDER BY stat_date需要注意的坑日期边界问题。如果你前端传的是2024-06-01到2024-06-30那么SQL的结束条件不能是 endDate因为sale_time是datetime类型2024-06-30 00:00:00之后的数据会被漏掉。正确做法是用 endDate 1 day把截止日期当作开区间。这个细节经常出bug而且不好排查。从这个报表还能延伸出好几个子模块近效期药品列表expire_date在90天内的批次、库存预警列表quantity小于stock_lower_limit的药品、供应商采购统计某段时间内从各个供应商进了多少货。这些功能虽然都是查询但正好覆盖了药店管理的几个核心痛点答辩的时候你可以主动介绍这些设计效果会很不一样。4. 权限控制、配置安全与常见坑4.1 基于JWT的多角色登录与拦截器既然确定了管理员、库管员、收银员三种角色那就不能只靠前端隐藏按钮来控制访问。真正的控制要在后端做。JWT登录流程大致是用户提交账号密码服务端校验通过后生成一个tokentoken里带上用户ID和角色信息前端每次请求在Header里携带这个token。后端在拦截器里解析token校验用户身份。这里有几个细节要提醒你token里不要放密码等敏感信息只放userId和roleCode这类必要的标识。拦截器要放行登录接口但其他接口都必须校验。接口级别再做一次权限校验比如管理员才能调用统计报表接口收银员不能访问入库相关接口。这个“登录校验 角色鉴权”组合用SpringBoot的HandlerInterceptor或者Sa-Token都能轻松实现。如果你用Spring Security也能做但对毕设来说配置成本偏高我更推荐HandlerInterceptor 自定义注解的方式轻量而且能体现出你对权限模型的理解。一个简单的自定义权限注解方案可以这样Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String[] value(); }然后在Controller方法上标注RequireRole({ADMIN, STOREKEEPER}) PostMapping(/purchase/confirm) public Result? confirmPurchase(RequestBody PurchaseRequest request) { return Result.success(purchaseService.confirm(request)); }拦截器里解析注解并判断当前用户的角色即可。刻意把这个逻辑拆成“登录拦截”和“角色校验”两步比全部混在一起更好维护也能让答辩老师看到你是有意识地做了分层。4.2 配置文件敏感信息处理SpringBoot的application.yml里面会写数据库账号密码、Redis密码等敏感信息你要是直接把明文写进去交到毕设文档里虽然也算常见但总归不太好看。如果让老师看到你连这个都注意了会加分不少。简单方案是用Jasypt一个加密组件对SpringBoot配置文件中的敏感信息进行加密。流程在SpringBoot中接入jasypt-spring-boot-starter依赖然后配置一个加密密钥将数据库密码等值通过工具类加密后放入配置文件程序启动时自动解密。spring: datasource: url: jdbc:mysql://localhost:3306/pharmacy?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: ENC(2xxFhxkJjLdBK6V7sD8Y8lS3Dy8e5Dxh)然后在JVM启动参数中指定密钥java -jar pharmacy-system.jar --jasypt.encryptor.passwordyourSecretKey这样即使别人拿到了配置文件没有密钥也解不开密码。为了保险起见不要在生产环境里把jasypt的密钥硬编码在application.yml中除非你对项目安全性没有太高的要求。4.3 事务、并发与事务失效事务是SpringBoot开发里最基础也最容易踩坑的点。在这个项目里createOrder()方法必须加Transactional这一点应该没有疑问。但有几个事务失效的经典场景我还是想拿出来啰嗦一遍因为这些情况在答辩代码里被问到的概率极大同一个类里方法A没加事务调用方法B加了事务B的事务失效。原因是Spring的事务是基于代理实现的同类内部调用走的是this.method()根本没经过代理对象。方法被非public修饰时事务不生效。被捕获异常导致事务不生效。Transactional默认只对RuntimeException回滚如果代码里catch住了异常并返回了正常结果事务自然就提交了。正确做法是catch后手动抛出RuntimeException或者用Transactional(rollbackFor Exception.class)。Transactional(rollbackFor Exception.class) public void confirmPurchase(Long purchaseId) { try { // 业务逻辑 } catch (Exception e) { log.error(确认入库失败, e); throw new RuntimeException(确认入库失败); } }这些细节平时写的时候觉得无所谓面试的时候可就是拉分项了。4.4 自带避坑清单不要用物理删除。药品、订单、用户这些核心数据全做逻辑删除加deleted字段用MyBatis-Plus的TableLogic注解很方便。这样误操作还能恢复而且统计报表也不会因为删除而缺数据。金额计算用BigDecimal不要用double/float。精度问题不是“差几毛钱”无所谓的问题而是电商医药管理系统里的一票否决项。日期字段建议用LocalDateTime别用java.util.Date。网上很多老教程还在用Date那是历史原因新代码不要学这个。药品编码、订单号、入库单号的唯一性要给足。生成规则建议用日期自增序列或者干脆用雪花算法MyBatis-Plus的IdWorker.getIdStr()可以直接用。打印小票功能如果不想接第三方打印SDK最简单的方案是后端返回一个结构化的结账单JSON前端直接调用浏览器打印或者用模板渲染成小票样式。这在毕设演示的时候也是个亮点。5. 面试准备与答辩加分点5.1 答辩时常见追问毕业设计答辩老师不会逐行看代码但会挑几个核心问题来验证项目是不是你自己做的。我把这个项目最可能被问到的几个问题整理了一下你提前准备好到时候能答上来场面会好看很多问销售出库的时候你是怎么做库存扣减的怎么避免超卖答按有效期FIFO查批次通过UPDATE语句里加“quantity deduct”条件实现原子扣减靠数据库行锁保证并发安全不用先查询再判断。问事务是在哪一层控制的如果中间扣了两次库存但后面某一条插入失败库存会不会对不上答整个createOrder都在一个事务里任何一个步骤抛出异常前面扣减的库存全部回滚不会出现对不上的情况。问药品设置了逻辑删除那历史订单里的药品信息还能查到吗答订单明细只关联drug_id但存了当时的名称和价格快照所以历史订单的记录是完整的这也是零售系统区别于普通CRUD的一个关键设计。5.2 给项目做几个“差异化”的升级如果时间允许我建议在基础功能之外挑一个方向做深一点。这不仅仅是为了答辩时显得有“差异化”更是为了让这个项目真正值得写进你的简历里。以下几个升级方向都值得考虑近效期药品自动预警写一个Spring Boot定时任务Scheduled每天扫描drug_batch表把有效期不足90天的批次自动生成预警记录by前端展示。如果还想再“上点高度”可以把预警记录接入告警消息。Redis缓存热门药品在药品查询接口上加Redis缓存热点药品详情直接走缓存。这一条能回答“你为什么用Redis”这个面试高频问题而且实现成本极低。集成一个数据大屏页面用ECharts做库存趋势、销售排行、近效期占比这几个可视化图表后台页面一下子就能提升一个档次。如果真想接触更完整的企业级技术栈可以看看Flowable工作流能不能用进采购审批这个场景。但记住这一步的前提是基础功能都稳了不然别把摊子铺太大毕设翻车不值得。6. 最后顺手分享几个实操心得项目做到这个程度该踩的坑你基本都会踩一遍这是好事踩过的东西记得才牢。我印象比较深的是很多同学写销售报表的时候查出来的金额总和总是对不上账排查半天发现问题出在“部分退货”上。退货单如果直接改了原销售单的金额那统计的时候按sale_order汇总就会和按明细汇总不一致。我的做法是退货单独生成退单记录原订单状态改成“部分退货”或“已退货”统计时用“销售汇总 - 退货汇总”来算净销售额。这样即使哪一天账对不上了也能顺着退单记录一条条追回去。还有一个建议是从第一天写代码就养成写接口测试的习惯。不用复杂的MockMvc就在项目里把手动测试的结果截图存着顺便把每个接口的入参出参整理一下到时候写毕业设计文档直接就能用省下大把时间。最后说一句。这个题目看起来是“又一个管理系统”但你把药品的批次效期、FIFO库存扣减、金额和事务这些点真正做到位之后它就不再是普通的CRUD练习了而是一个有完整业务闭环的项目。认真做完无论答辩还是面试你都会有东西可讲、有底气可说。