去年帮一家社区药店做管理系统我原本以为就是个普通进销存结果深入了解之后发现完全不是那么回事。药店的库存管理核心痛点根本不是还有多少货而是这批货到没到期、下次进货该先卖哪一批。通用进销存软件都是按普通商品设计的压根不处理批号和有效期这两个字段所以我才决定用SpringBootVueMyBatisMySQL自己搭一套前后端分离的Web药店管理系统把药品档案、批次库存、效期预警、销售收银、员工权限全部串在一起。这篇文章我会把项目的需求拆解、技术选型、数据库建模、后端核心链路、前端联调和部署上线整个过程完整梳理一遍里面也包含不少我实际踩过又填平的坑。适合正在做后台管理系统、想完整了解前后端分离项目从0到1落地全流程的开发者参考。1. 药店管理的核心痛点拆解为什么普通进销存不够用1.1 药品和普通商品最大的区别批号与有效期超市卖可乐同一个SKU卖完就补货过期损失顶多退给供应商。药店卖药不行每一批进货都有独立的批号、生产日期、有效期门店售卖时必须先进先出也就是先到期的批次先卖。如果系统不支持按批次管理库存哪怕总数算对了也说不清货架上的那一盒到底是哪一批、还剩几天到期。所以这套系统的第一设计原则就是药品档案只保存这是什么药真正的库存必须落到批次这个维度上。每次采购入库系统按照供应商和采购单生成批次库存记录每次销售后端自动匹配最早到期的批次进行扣减。这个逻辑贯穿整个库存模块是整个数据链的核心。1.2 功能模块拆解从药品档案到销售收银的完整清单按照药店的真实业务流程我把系统分成了五大模块药品管理药品分类、药品档案通用名、商品名、规格、厂家、批准文号、剂型、单位、处方类型、零售价、进价、库存上下限、药品上下架。采购与入库供应商档案、采购单创建与审核、入库单生成、批次库存增加。库存管理实时库存查询、批次库存明细、效期预警、库存上下限预警、盘点。销售收银购物车式收银台、销售单、销售明细、退货、多种支付方式、会员折扣。系统管理用户、角色、菜单权限、操作日志。和普通进销存对比明显多出了效期预警批次明细处方类型这类药店专属能力销售结算也更复杂。这也是后面设计表结构和接口时的重点对象。1.3 角色权限谁在用这套系统权限怎么分一家社区药店的日常角色通常包括老板、店长、药师、收银员。各角色权限必须分开来管收银员只能开销售单、查库存不能改药品价格药师可以维护药品档案和批次但不能删销售记录店长能看报表和做盘点老板能看全部数据、管理员工账号。这里我使用了经典的用户—角色—菜单三级模型用户表、角色表、菜单表再加上用户角色关联表和角色菜单关联表。后端接口统一做登录校验和角色校验前端路由也按角色生成动态菜单。这套模型并不复杂但对于中小药店的权限管理来说已经完全够用而且后续要加新的功能菜单只需要往菜单表和角色菜单表里插数据即可。2. 技术选型复盘SpringBootVueMyBatis这套组合的取舍过程2.1 后端选型为什么不是微服务也不是直连JDBC药店管理系统这类业务系统核心要求是交付快、运行稳、好维护。SpringBoot天然满足这些需求内嵌Tomcat打包成一个jar就能跑起步依赖把Web、MyBatis、MySQL驱动这些组件一键引入配置集中在application.yml里部署时改改数据库连接和端口就行。数据持久层我选的是MyBatis不是MyBatis-Plus原因不是后者不好而是这个项目的查询大部分是多表关联查询药品分类供应商批次库存手写SQL的可控性更强也能更清楚地看到库存扣减这类语句在数据库层面到底是怎么执行的。直连JDBC当然也能做但Mapper XML把SQL、参数映射和结果集映射统一管理能省掉大量样板代码。在这个规模的项目里MyBatis是性价比最高的选择。至于微服务我几乎没有犹豫就直接排除了。药店系统数据量、并发量都有限单应用加单库就是最简单的可靠方案。引入微服务等于把注册中心、网关、服务调用链这些复杂度全部压给自己完全没这个必要。2.2 前端选型Vue配合组件库表单类业务开发效率最高前端的核心场景是大量表单、表格、弹窗、权限菜单。Vue的响应式数据绑定和组件化结构天然适合这类后台页面。我给前端搭配了一套成熟的Vue UI组件库表格、表单、日期选择器、消息提示这些现成组件直接拿过来用页面从0到1的速度非常快。版本上我选的是Vue 2.7主要是因为老牌的组件库生态和文档资料最全踩坑经验也多。如果团队熟悉Vue 3的组合式API用Vue 3也完全可以核心接口设计不变。状态管理用Vuex路由用Vue RouterHTTP请求用axios都是后台管理开发的标准搭配。这套组合看起来朴素但实际开发效率真的高维护成本也低。2.3 开发环境与版本组合建议我自己用的环境组合如下表技术版本说明JDK1.8稳定兼容性好Spring Boot2.7.x2.x生命周期内版本踩坑资料多MyBatis3.5.x手写SQL控制力强MySQL5.7或8.08.0功能更强两个版本都能跑Vue2.7配合经典组件库最稳Node.js14前端构建环境这套版本组合的兼容性经过大量项目验证没必要刻意追求最新版本。做管理系统真正要花心思的是业务逻辑能不能跑通框架版本只是实现手段。版本选得太新反而容易遇到文档还没跟上、插件不兼容的问题。3. 数据库表设计批次库存、效期预警、销售流水是怎么建模的3.1 核心表清单先把这套系统的核心表列出来表名用途sys_user用户账号sys_role角色sys_menu菜单权限sys_user_role、sys_role_menu用户角色、角色菜单关联表drug_category药品分类drug_info药品档案supplier供应商purchase_order采购单purchase_order_item采购单明细stock_batch批次库存stock_record库存流水sale_order销售单sale_order_item销售明细member会员总共15张表左右覆盖药店日常经营的一整条链路。这个数量对于管理系统来说是合理的既没有为了看起来专业而拆出几十张过度设计的表也没有少到没法支撑业务。3.2 药品档案与批次库存的关键字段drug_info表的设计要点在于档案和库存分离。它负责描述药品本身核心字段有id、category_id外键、drug_name药品名称、common_name通用名、specification规格比如24粒/盒、manufacturer生产厂家、approval_number批准文号、dosage_form剂型、prescription_type处方类型、unit最小单位、purchase_price和sale_price进价与零售价以及min_stock和max_stock库存上下限用于预警。stock_batch是我这个项目里最核心的表字段看起来简单但决定了效期管理能不能落地id、drug_idbatch_no批号production_date生产日期、expire_date有效期quantity当前剩余数量supplier_id供应商purchase_price这批药的进价为什么非要单独建批次表因为同一款药可能相隔几天分别进货批号不同、有效期不同、进价也可能不同。把批次拆出来销售时才能精确判断先出哪一批。效期预警直接按expire_date字段筛选比如找出90天内到期的批次操作起来非常直观。3.3 库存流水所有库存变化必须可追溯在药店系统里库存数量不能想改就改每一笔变化都要有来龙去脉。stock_record的核心字段包括drug_id、batch_id、change_type变化类型入库、销售、退货、盘点调整等、quantity_before、quantity_change、quantity_after变动前后的数量、order_no关联单号、operator_id操作人、create_time时间。这样设计之后任何时候都能回答三个问题这批货还剩多少、是从哪一批进来的、谁在什么时间动过它。盘点时如果出现差异也可以从流水反查是哪一步出了问题。这个写流水的习惯在项目初期看似多花了一点功夫后期排查问题时却能节省大量时间。3.4 并发扣减库存的SQL写法药店销售的高并发程度不算高但两个收银员同时卖同一盒药这种场景还是可能发生库存扣减必须防超卖。最简单可靠的做法就是条件更新UPDATE stock_batch SET quantity quantity - #{num} WHERE id #{batchId} AND quantity #{num}这条UPDATE语句自带行锁先判断数量足够再扣减扣完返回受影响行数。如果返回0说明库存不够前端就提示库存不足。整个操作再放到事务里把扣批次库存写库存流水生成销售单放在同一个方法中任何一个环节失败就全部回滚。这套方案足够稳健不需要引入Redis分布式锁那样的重型组件。注意查询可用批次时一定要按有效期最早的优先排序。否则可能出现明明有库存却先扣掉了有效期较长的货导致短效期药品最终压在仓库里过期。4. 后端关键链路实现入库、库存扣减、销售结算的代码逻辑4.1 统一返回结构与全局异常处理所有Controller接口都返回同一个Result对象code为200表示成功非200表示业务异常前端根据code统一弹出提示。这样前端只需要写一套响应拦截器不需要为每个接口各做一套状态判断。全局异常处理用RestControllerAdvice配合ExceptionHandler把参数校验异常、业务异常、数据库异常分别映射成友好提示。实际开发里最烦的就是空指针异常堆到前端页面直接白屏。有了全局兜底就算内部报错前端也只会看到一句明确提示排查起来也方便得多。4.2 登录认证与权限校验的轻量实现登录接口校验用户名密码之后生成一个带用户信息的Token返回给前端前端存到localStorage里。后续请求在axios拦截器中统一加上Authorization头。后端用一个HandlerInterceptor统一校验Token解析失败直接返回401。需要做角色控制的接口用注解标记允许的角色编码校验不通过返回无权限。这里没有引入完整的OAuth2框架原因是内部管理系统的信任边界很清晰员工数量少、角色固定一套轻量级Token方案比引入整个认证框架更省心出问题也更容易定位。4.3 入库流程采购单、入库单、批次库存入库的完整链路是这样填写采购单包含供应商、药品、数量、批号、有效期、进价确认入库时校验采购单状态然后在事务里执行两步操作给stock_batch新增一条批次库存数据再给stock_record写一条变化量为N的流水。最后把采购单标记为已入库。有一个细节很容易被忽略同一药品、同一批号、同一进价连续两次入库要不要合并成一条批次记录我的建议是合并。入库前先按drug_id和batch_no查询存在就累加quantity不存在才新建记录。这样批次库存列表更干净效期预警也不会因为同一个批号分成多行而重复提示。4.4 销售流程先进先出扣减与结算销售流程是另一条完整链路我拆成六步前端提交购物车明细药品ID、数量、支付方式、会员ID。后端生成销售单号遍历每个药品明细。对每个药品按有效期最早优先查询当前可用的批次列表。逐批次扣减库存某个批次不够就继续扣下一批直到数量扣完或库存不足。每扣完一个批次生成一条出库流水。汇总所有明细金额计算会员折扣生成销售单主表和销售明细表。整体包在Transactional里核心片段大概是这样的逻辑Transactional(rollbackFor Exception.class) public SaleOrder createSale(SaleRequest request) { // 遍历购物车中的每个药品 for (SaleItem item : request.getItems()) { int remain item.getNum(); // 按有效期最早优先查询可用批次 ListBatchStockVO batches stockBatchMapper.selectAvailableBatches( item.getDrugId(), remain); for (BatchStockVO batch : batches) { int deductNum Math.min(remain, batch.getQuantity()); int updated stockBatchMapper.deductStock(batch.getId(), deductNum); if (updated 0) { throw new BizException(库存不足); } stockRecordMapper.insert(buildOutRecord(batch.getId(), deductNum)); remain - deductNum; if (remain 0) { break; } } } // 计算总金额、会员折扣插入销售主表和明细表 return saleOrder; }这里最关键的就是循环扣减多个批次同一盒药可能分布在多个批次里必须按顺序逐个扣直到把需求数量扣完。如果中间某一步扣减失败整个事务回滚销售单也不会残留在数据库里。代码量不大但把先进先出这个业务规则完整落到了数据库操作上。5. 前端页面与接口联调Vue后台管理里最容易被坑的两个问题5.1 页面结构与路由设计前端页面按模块划分登录页、首页看板、药品管理、采购入库、库存查询、效期预警、销售收银、会员管理、系统管理。路由全部放在Vue Router中登录成功后再根据当前用户的角色拉取菜单权限动态注册有权限的路由。没有权限的页面不会出现在侧边栏菜单里直接手输URL访问也进不去。销售收银页是整个系统交互最复杂的页面左侧是药品搜索和选择区域右侧是购物车列表底部是合计和结算按钮。因为表单里频繁出现有效期这类日期字段组件库的日期选择器在这一页帮了大忙。搜索药品时我做了防抖处理避免每次输入都向后端发一次请求。5.2 axios封装与跨域处理前端开发时后端接口跑在8080前端页面跑在另一个端口必然遇到跨域问题。后端可以用CrossOrigin或者配置CorsFilter解决但我更推荐在开发阶段用前端代理来转发// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } };前端所有请求统一以/api开头开发阶段由devServer把请求转发到后端浏览器端始终是同源访问。生产环境部署时再让Nginx做一层/api反向代理思路完全一致。axios拦截器这里有一件事必须做请求拦截器统一加Token响应拦截器统一处理401跳转登录页。否则用户Token过期后每个接口报错都要单独处理体验很差代码也会变得很啰嗦。5.3 刷新页面404问题的排查过程这是前后端分离项目里最容易踩的一个坑。开发时一切正常打包部署后在某个路由页面按F5刷新直接出现404。原因很简单前端路由用了history模式刷新时浏览器会按完整路径去服务器找文件但服务器上只有dist目录里的index.html根本没有 /products/xxx 这个真实文件。我在Nginx配置里加一行try_files就解决了server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:8080; } }它的意思是找不到对应的静态文件时统一回到index.html由前端路由接管路径解析。这个配置很常见但很多新手第一次部署前后端分离项目时常常在这里卡上一整天所以我单独拿出来提醒一下。6. 完整部署过程从源码拉取到浏览器访问的每一步6.1 环境准备清单部署之前先确认机器上有这些环境JDK 1.8Maven 3.6用来编译打包后端Node.js 14只在构建前端时需要MySQL 5.7或8.0Nginx生产环境中托管前端打包产物单台服务器装齐这些就够了。数据库初始化脚本放在项目的sql目录下执行一条命令完成建库建表和基础数据初始化mysql -uroot -p init.sqlinit.sql会建库、建表并插入默认的管理员账号和基础菜单数据省去手动创建的麻烦。6.2 后端打包与配置修改后端配置在src/main/resources/application.yml里部署时重点关注数据库连接和端口这两块server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/pharmacy_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password然后执行打包命令mvn clean package -DskipTests打包成功后target目录下会生成pharmacy-admin.jar。直接运行时用nohup java -jar pharmacy-admin.jar app.log 21 用nohup保证关掉终端后程序继续运行日志重定向到app.log出问题打开日志看就行。6.3 前端构建与Nginx托管前端项目依次执行npm install npm run build构建产物生成在dist目录。把dist下的所有文件上传到服务器放到Nginx配置指定的html根目录里再配上前面那段server配置。最后检查并重载Nginxnginx -t nginx -s reload浏览器访问服务器IP出现登录页面登录后能正常操作整个系统就算上线了。6.4 常见启动失败排查清单下面这些是我实际遇到比较多的启动期问题列成一个速查表现象排查方向后端启动报数据库连接失败检查数据库地址、端口、账号密码确认MySQL已启动确认init.sql已执行前端多个接口返回401确认登录后Token已写入localStorage确认axios拦截器加了Authorization头页面能打开但刷新就404检查Nginx的try_files配置npm install报网络错误配置国内镜像源后重新安装jar启动后端口被占用用查看端口命令确认进程换端口或清理占用进程尤其是前两个问题最容易造成开发环境正常、生产环境抽风的错觉遇到时先怀疑配置不要急着改代码。7. 上线后的真实反馈与扩展方向7.1 门店使用后的高频功能系统真正用起来之后店长反馈最常用的功能其实是效期预警。每天打开系统先看90天内到期的药品清单提前做促销或者联系供应商退换直接减少了过期损失。这也让我更加确认药店管理系统表面上看是进销存实际上效期管理才是经营者最关心的命门。其他功能做得再好如果效期处理不到位都谈不上是合格的药店系统。另一个高频页面是销售收银收银员一天要开几十单查询药品如果超过一秒就会觉得卡顿。我在优化时给药品搜索接口加了索引前端做了本地缓存和分页最终把查询耗时压到了可接受的范围。这类优化不需要多高深的技术但实际体验提升非常明显。7.2 这套系统还能往哪些方向扩展对接扫码枪药店收银台扫码枪是标配把条形码扫描直接接入药品查询收银效率能提高一大截。报表与分析按天、按周生成销售汇总、毛利分析、滞销药品排行经营者会很需要。会员储值卡把支付方式扩展为储值卡余额扣款需要新增会员钱包表和充值、消费流水。供应商协同开通供应商登录窗口让供应商在线处理采购订单减少电话沟通成本。这套系统的技术栈足够朴素扩展方式也很灵活。业务上每加一个模块后端加一组Controller和Mapper前端加一个路由页面就能接上不会因为架构复杂被卡住。就我自己的体会而言做这类前后端分离的管理系统技术选型真的不用贪新把批号、效期、流水这些业务约束设计清楚比堆任何框架都管用。这套药店系统从数据库到页面再到部署最花时间的地方不是写接口而是把哪批货先卖这条业务主线彻底想明白。如果你也在做类似的进销存或后台管理系统建议先把手里的核心业务流程完整画一遍再动代码。我在这个项目里最大的收获就是业务理解得越深代码结构反而越简单。最后分享一个小技巧——效期预警的提醒阈值不要写死成90天把它做成系统参数让不同药店根据自身进货周期灵活设置这个细节用起来非常加分。