资讯详情

SpringBoot+Vue药店销售系统开发实战:从进销存到库存效期管理

📅 2026/10/12 14:03:37 | 华诺云谱 👁 阅读
SpringBoot+Vue药店销售系统开发实战:从进销存到库存效期管理
提到药店销售系统很多人的第一反应是“不就是个进销存吗”但真正在店里跑过一圈就会发现药品零售和普通商品零售差别很大药品有批号、有有效期、有拆零销售、有处方药登记会员还往往要按积分兑礼品采购入库、库存盘点、效期预警、统计报表串在一起才是一个完整的业务闭环。我用SpringBoot做后端、Vue做前端完整搭建了“康健药店销售系统”这套前后端分离项目从数据库建模到单据流转再到权限控制把整个开发过程的思路、选型理由和实际踩坑记录整理出来给正在做类似系统或者准备拿它做课程的开发者一份能直接落地的参考。无论你是刚接触SpringBootVue的新手还是想快速搭一套中小型药店管理系统的开发者这篇内容都适合你。1. 项目定位与整体架构拆解1.1 药店销售系统的独特需求开发系统之前我专门去一家社区药店待了半天观察店员从接单到收银的完整流程。真实场景里店员输入药品名称时往往只记得商品名的一部分所以系统必须支持模糊搜索药店的药品有“整盒”和“拆零”两种销售方式有的药按盒卖有的则按片卖数据模型里必须有销售单位换算再有就是有效期同一款药可能有两个批号系统得能优先出效期更近的批次否则过期药压在库里就是纯亏。这套“康健药店销售系统”最终定位为面向中小型单体药店的进销存会员统计一体化管理系统。核心功能包括药品信息维护、采购入库、销售开单、库存管理、效期预警、会员管理、供应商管理、操作员权限和销售统计报表。相比通用电商系统它更强调库存精度和医药场景的合规性比如销售处方药时需要登记购药人信息这个细节后端表和前端表单都专门预留了字段。1.2 前后端分离架构为什么选 SpringBootVue很多人在技术选型时会纠结到底用单体JSP还是前后端分离。我的判断很简单项目需要同时跑在门店局域网和远程访问场景前端要兼顾PC浏览器和平板端操作后端要能独立提供接口给以后可能的移动端复用那前后端分离就是必然选择。SpringBoot负责提供RESTful APIVue负责页面交互和数据展示两者通过JSON通信互不干扰。SpringBoot的优势在于起步快、生态成熟内置Tomcat配合MyBatis-Plus操作数据库非常顺手很适合这类以CRUD为主的中小型管理系统。Vue这边我用的是Vue 2 Element UI组合一方面Vue的组件化开发让销售开单这种复杂交互页面可以拆成“商品选择”“明细列表”“结算信息”等多个独立组件另一方面Element UI的表格、表单、弹窗组件能大幅缩短后台管理页面的开发时间。前后端分离还有一个好处是团队协作时可以并行开发只要接口约定好互不阻塞。1.3 目录结构与核心模块划分前后端分两个工程目录后端mall-admin-style的经典分层前端按页面和组件维度组织。后端目录结构大致如下src/main/java/com/kangjian/ ├── common/ // 统一返回结果、全局异常、常量 ├── config/ // MyBatis-Plus配置、拦截器注册、跨域配置 ├── controller/ // 控制层接收请求并返回结果 ├── service/ // 业务层核心业务逻辑 ├── mapper/ // 数据访问层 ├── entity/ // 实体类 ├── dto/ // 请求和响应对象 ├── utils/ // JWT工具、日期工具等 └── KangjianApplication.java前端使用Vue CLI创建标准工程src/ ├── api/ // 封装所有后端接口请求 ├── router/ // 路由配置含动态权限路由 ├── store/ // 登录状态、用户信息管理 ├── views/ // 页面视图 │ ├── sale/ // 销售开单 │ ├── stock/ // 库存管理 │ ├── purchase/ // 采购管理 │ ├── member/ // 会员管理 │ └── report/ // 统计报表 ├── components/ // 通用组件 └── utils/ // 请求封装、格式化工具模块划分上我把系统拆成七个核心模块药品管理、采购管理、销售管理、库存管理、会员管理、统计报表、系统管理。其中系统管理包含用户登录、角色权限和操作日志。销售开单是系统的核心主流程采购入库和库存管理为它提供数据基础会员和统计模块则在业务支撑之上做增值功能。2. 核心业务模块设计与数据建模2.1 业务模块拆解从药品入库到开单销售一条完整的业务链路是这样的先在基础信息里维护药品和供应商然后做采购入库入库时选择供应商、药品填写数量、单价和批号、有效期系统自动增加库存销售时选择会员或按散客处理添加药品、填写数量系统校验库存充足后扣减库存并生成销售单每天结束后店长查看销售统计报表同时系统对所有药品做效期预警即将过期的药品会进入预警列表。整个链路里采购入库和销售开单是关键节点任意一个环节出错都会导致库存数据失真。采购入库我特意做了“采购单入库单”合一的模式采购单审核通过后直接生成入库流水并更新库存避免二次录入。销售开单采用“主表明细表”结构主表记录销售单号、会员ID、应收金额、实收金额、收款方式、售货员等汇总信息明细表逐条记录药品、数量、单价、金额。这样设计能保证一张单据对应多行明细统计报表可以按主表聚合维度统计也可以按明细表分析药品销售排行。2.2 数据库表设计核心要点表结构是整个系统能不能撑住业务的关键。我总共设计了12张核心表最有代表性的几张如下drug药品表药品id、名称、通用名、规格、生产厂家、批准文号、零售价、会员价、是否拆零、拆零单位、拆零售价、处方药标识、状态。stock库存表库存id、药品id、批号、生产日期、有效期、库存数量、预警阈值。purchase_order采购单表采购单号、供应商id、采购员id、采购总金额、状态、入库时间。purchase_order_item采购明细表采购单id、药品id、批号、生产日期、有效期、采购价、采购数量、入库数量。sale_order销售单表销售单号、会员id、销售员id、应收金额、实收金额、收款方式、处方药登记信息、销售时间。sale_order_item销售明细表销售单id、药品id、批号、销售价、销售数量、小计金额。member会员表会员id、会员卡号、姓名、手机号、积分、累计消费额、等级、创建时间。supplier供应商表、user用户表、role角色表、user_role用户角色表、operation_log操作日志表。盘点一下设计原则凡是涉及历史单据的数据药品名称、规格、单价这些字段一定要冗余存储到单据明细表里不能只存药品id再去关联当前药品表。因为药品信息将来可能改价、改名历史单据必须保留当时的快照数据这也是药店财务审计的基本要求。2.3 库存与效期管理的设计思路库存设计上我踩过第一个坑就是“只存一个总库存数字”。实际药品必须按批号管理同一款药不同批号进价不同、有效期不同销售时也要按批号追踪。所以库存表以drug_id batch_no为维度每一批入库药品生成一条独立的库存记录库存扣减时优先扣除距离有效期最近的批次也就是业内常说的FEFO策略。效期预警的逻辑在后端Service里定时执行每天扫描所有库存记录计算距离有效期的天数当剩余天数低于预警天数阈值时把药品写入预警结果列表。阈值我做成系统参数默认设置是30天提醒一次、7天紧急预警。这个功能虽然代码很简单但药店实际很需要因为过期药不能销售处理损失完全由门店承担早一点预警就能通过促销等方式减少损耗。库存数量和批号数据的关联也给销售开单增加了复杂度。前端选择一种药品后如果存在多个批次需要弹出批次选择窗口让操作员确认如果设置了“自动先进先出”也可以直接由后端按FEFO规则自动分配批次。我两种模式都做了默认为自动分配但保留手工选批次的入口。3. 后端核心实现与实操要点3.1 SpringBoot 项目搭建与分层规范后端我使用的是SpringBoot 2.5.5JDK 1.8MyBatis-Plus 3.5.1MySQL 5.7。创建项目时直接通过IDEA里的Spring Initializr生成勾选Web、MySQL Driver、MyBatis依赖。这里有一个重要建议不要一上来就追求各种最新版本的依赖SpringBoot 2.x搭配MyBatis-Plus 3.5.x已经非常稳定网上能找到的解决方案也最多遇到问题好排查。使用SpringBoot 3.x则需要注意jakarta命名空间的变化老教程可能不适用。分层规范方面我严格遵守Controller → Service → Mapper的单向调用关系Controller不做业务运算只负责参数接收和结果返回Service层处理事务和核心逻辑Mapper层负责数据库操作。实体类用MyBatis-Plus的TableName、TableId注解映射表字段逻辑删除字段用TableLogic标注这样删除药品信息时执行的是逻辑删除不会破坏历史单据的关联数据。分页方面MyBatis-Plus提供了内置的分页插件需要一个配置类注册PaginationInnerInterceptor。注册后分页查询统一使用PageT对象作为参数和返回值Controller接收current和size两个请求参数前端就能和Element UI的el-pagination组件直接对接。部分查询涉及多表关联时需要手写XML但尽量只在复杂统计里用简单的列表查询尽量用MyBatis-Plus的LambdaQueryWrapper可读性和维护性都好很多。3.2 统一返回结构与全局异常处理后端接口如果各写各的返回格式前端联调会非常痛苦。我定义了一个R对象作为统一返回结构包含code、message、data三个字段public class R { private Integer code; private String message; private Object data; public static R ok() { return ok(null); } public static R ok(Object data) { R r new R(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static R error(String message) { R r new R(); r.setCode(500); r.setMessage(message); return r; } }业务异常统一通过自定义BusinessException抛出然后由RestControllerAdvice全局异常处理器捕获转换成对应的R对象返回给前端。这样设计的收益在联调阶段体现得最明显前端只需要在axios拦截器里判断code 200即可其他情况统一弹错误提示不需要每个接口各自做判断。全局异常里我还额外捕获了MethodArgumentNotValidException这样后端使用Validated做参数校验时校验不通过的信息可以精确返回给前端。这里分享一个细节不要把RuntimeException一股脑都返回“系统异常”前端排查问题时毫无头绪。正确的做法是错误信息尽量具体比如“库存不足当前剩余5盒”“药品已过期不允许销售”前端直接把message展示出来操作员一眼就能明白问题出在哪。3.3 基于JWT的登录鉴权与操作员权限控制登录鉴权我采用JWT方案用户登录成功后生成一个包含userId、username、roleCode的token有效期为8小时。前端把token存在本地存储里每次请求在axios拦截器里带上Authorization头。后端用HandlerInterceptor实现登录拦截写一个AuthInterceptor在preHandle里解析token解析失败直接返回401状态码前端收到401则自动跳转登录页。权限控制这块我没有引入复杂的Shiro或Spring Security而是基于“角色菜单/按钮权限”做了轻量控制。用户表关联角色表角色表里存权限标识串比如“sale:add”“stock:list”“report:view”。后端在Controller层方法上使用自定义注解RequirePermission(sale:add)通过AOP切面校验当前用户是否包含对应权限不通过则抛出无权限异常。这样做的好处是代码侵入小权限逻辑集中前端菜单也可以根据用户权限标识动态渲染。需要特别注意JWT本身只能保证请求者的身份不能保证权限所以每次请求都必须重新校验角色的权限配置而不是只信任token里的roleCode。角色权限调整后已登录用户需要重新登录才能刷新token里的权限信息这是JWT方案的一个固有取舍我在系统里也做了“修改角色后强制用户下线”的处理稳妥很多。3.4 MyBatis-Plus 下的动态查询与分页药品列表、销售单列表这类查询往往有多个筛选条件药品名称、分类、厂家、状态、日期范围等前端传过来的参数是动态的有些可能为空。用MyBatis-Plus的LambdaQueryWrapper处理动态条件非常方便LambdaQueryWrapperDrug wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(query.getKeyword()), Drug::getName, query.getKeyword()); wrapper.eq(query.getCategoryId() ! null, Drug::getCategoryId, query.getCategoryId()); wrapper.eq(query.getStatus() ! null, Drug::getStatus, query.getStatus());这段代码的含义就是“如果参数有值就追加对应查询条件如果参数为空就不处理”。这样写避免了大量if-else拼接SQL的丑陋代码也能有效防止前端传入空参数导致全表查询的问题。分页查询的返回结果直接用PageDrug对象返回里面已经包含了total和records字段前端拿到之后可以直接赋值给表格和分页组件。事务管理也是后端一个重点。销售开单和采购入库都涉及多张表的写操作必须在Service方法上加上Transactional注解保证业务操作要么全部成功要么全部回滚。我在做销售开单时流程分为生成销售单主表、逐条生成销售明细、扣减库存、更新会员积分四个步骤任何一个环节抛异常都要求前面的写入全部撤销否则就会出现“主表生成了但明细缺失”或者“库存扣了但单据没生成”的脏数据。事务是SpringBoot里最容易忽略但最不能忽略的细节。4. 前端 Vue 端实现与联调细节4.1 Vue 项目初始化和路由设计前端使用Vue CLI 4.x创建项目组件库选择Element UI状态管理使用Vuex。初始化后第一件事是安装vue-router、axios、element-ui、sass等依赖然后配置路由。路由设计分两层第一层是登录页独立于主布局之外第二层是以Layout为父组件的业务页面所有需要菜单导航的页面都挂在它下面。路由守卫很关键。在router.beforeEach里判断用户是否已经登录没有token直接跳登录页有token的情况下再判断当前路由的meta中是否声明了所需权限如果没有权限就拦截并跳转无权限页面。动态路由方面我最初想把菜单完全由后端返回并动态注入路由后来发现中小型系统用静态路由按钮级权限控制就够了完全动态路由虽然炫酷但会引入刷新页面后路由丢失的坑没必要增加复杂度。Element UI的菜单组件el-menu配合el-submenu渲染二级菜单我封装了一个Sidebar组件根据路由配置中带meta.title的节点动态生成菜单新增页面时只需要在路由配置里加一行菜单自动出来非常省事。4.2 页面组件划分与业务表单设计前端的核心页面是销售开单页它是整个系统交互最密集的地方。开单页面上我拆成了四个组件左侧药品搜索区、中间已选药品明细表格区、右侧结算信息区、底部操作按钮区。药品搜索区的核心是el-autocomplete或el-select启用远程搜索输入关键词后调用后端药品搜索接口返回的结果展示药品名称、规格、零售价和库存选中后追加到明细表格。这里有一个交互细节追加药品时需要检查明细里是否已经存在同批号同药品的记录如果存在则直接累加数量而不是新增一行否则开单页面会越拉越长。明细表格使用el-table每一行可以编辑销售数量数量变化时实时计算小计金额和总金额。结算信息区展示应收金额、会员折扣、实收金额收款方式支持现金、微信、支付宝等。药品在加入明细时后端返回的数据里已经有库存和批次信息前端就会在数量输入框上做最大值控制超过当前批次库存时直接提示并阻止输入。这个看似简单的前端校验实际能拦截大部分因为手误导致的超卖问题。采购入库页面和开单页的结构类似区别是多了供应商选择、采购价、批号和有效期字段。我给批号和有效期专门做了一个弹窗式表单因为一个采购单可能包含多个药品每个药品的批号有效期都不同展开在表格里会显得很乱。弹窗里提交之后把数据推回明细被提交的药品行会自动标记为“已维护批次”。4.3 Axios 拦截器与后端联调的坑前后端联调阶段是最容易出问题的地方。我封装了一个request.js统一配置axios的baseURL、超时时间请求拦截器里自动添加token响应拦截器里统一处理返回数据。具体逻辑包括后端返回code ! 200时直接弹出this.$message.error(message)业务代码不再重复处理错误。后端返回401时清除本地token跳转登录页。网络异常或超时统一提示“网络请求失败请检查网络连接”。开发环境下前端默认请求http://localhost:8080/api后端所有Controller的RequestMapping统一加了/api前缀同时后端配置了全局跨域支持。这里要提一个我踩过的坑SpringBoot的跨域配置如果同时使用了CrossOrigin注解和WebMvcConfigurer的跨域配置可能出现重复的Access-Control-Allow-Origin头浏览器直接报错。我的经验是只保留WebMvcConfigurer里的全局配置不要到处加注解避免混乱。另一个常见坑是时间格式。后端返回的LocalDateTime默认序列化成2025-01-18T21:30:00带字母T且不够直观前端展示很难看。我统一在后端配置了Jackson的日期格式化全局把LocalDateTime转成yyyy-MM-dd HH:mm:ss格式前端就不需要每个字段都单独处理了。5. 部署、验收与常见问题排查5.1 本地联调、打包与部署整个项目在本地的运行流程是先启动MySQL执行项目里的init.sql脚本初始化数据库和测试数据再启动后端SpringBoot应用确保端口8080没有被占用最后执行npm run serve启动前端开发服务浏览器访问localhost:8081就能进入登录页。为了方便联调我在初始化脚本里创建了一个管理员账号admin和测试账号operator前者拥有全部权限后者只有销售开单和库存查看权限。打包部署可以分成两步走。后端使用Maven打包执行mvn clean package生成可执行jar然后通过java -jar命令运行前端执行npm run build生成dist静态资源目录可以交给Nginx部署。正式环境下我通常会建议把前端放在Nginx里同时利用Nginx反向代理将/api路径转发到后端8080端口这样前后端就同域了也完全避开了跨域问题server { listen 80; server_name your-domain; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }这里try_files $uri $uri/ /index.html很重要否则Vue的路由在history模式下刷新页面会出现404。如果不想配置Nginx也可以改用hash模式路由但URL里会多一个#号观感稍差我还是推荐history模式配合Nginx配置。5.2 常见问题速查表整理了我在开发和调试过程中实际遇到的高频问题表格形式方便直接查看问题现象可能原因解决方案前端请求接口返回404前端baseURL的/api前缀和后端Controller路径不一致统一检查后端所有RequestMapping是否带/api前缀后端返回的日期格式带字母T未配置Jackson的日期格式化在application.yml中全局配置yyyy-MM-dd HH:mm:ss浏览器提示跨域请求失败后端未配置跨域或者同时用了注解和全局配置产生重复头只保留WebMvcConfigurer里的跨域配置配置allowedOrigins和allowedMethods登录接口成功但后续请求401token未在axios请求拦截器里添加或token过期时间太短debug检查请求头里是否有Authorization确认token有效期采购入库后库存没有增加Service方法缺少事务异常未回滚或未调用库存更新方法检查入库流程是否在同一事务内确认入库后stock表新增或累加记录前端一键build后页面空白静态资源路径问题在vue.config.js里配置publicPath: ./或按Nginx部署方式使用绝对路径列表查询很慢且数据混乱多表查询分页未生效确认MyBatis-Plus分页插件已注册多表查询使用Page参数并传入pageNum、pageSize密码明文存在数据库安全习惯不好使用BCrypt加密存储登录时比对加密后的密文安全方面还有几个细节提醒数据库的密码不要和业务表主键字段混为一谈用户表密码字段一定要加密存储接口层面对于销售开单、库存修改这类写操作要有基本的权限校验不能让普通操作员随意调用管理员接口前端也不能完全信任所有的数据校验后端必须重复做一遍。5.3 安全与数据一致性的几个细节药店销售系统的数据一致性比普通进销存要求更高因为涉及金额、库存、药品效期这些直接关系到钱和合规的字段。我在开发中主要盯住了四个点第一销售开单和库存扣减必须处于同一事务内避免出现“单据生成了但库存没扣”或“库存扣了但单据没有”的中间状态。第二库存扣减使用乐观锁或悲观锁处理并发场景我采用了在库存更新SQL里增加stock_quantity updateNum条件的方式更新影响行数为0时说明库存不足直接抛出异常简单可靠。第三销售单号采用“日期随机数”格式生成避免并发下重复同时给销售单主键字段加唯一索引作为兜底保证。第四操作日志记录了每个用户对药品价格、库存数量的修改操作方便事后审计。从开发节奏来看我的建议是先做“药品管理销售开单库存扣减”这条最小闭环让系统先能跑起来卖掉一盒药然后再补采购入库、会员积分、统计报表等外围功能最后才做角色权限和操作日志这些锦上添花的部分。很多初学者一上来就大量时间花在框架配置和权限设计上结果核心业务还没跑通这是最常见的误区。6. 实操复盘与后续扩展思路做完这个系统之后我最大的感受是“系统开发不是写代码而是梳理业务”。药店这个场景里最复杂的不是SpringBoot或Vue的技术问题而是如何让库存、批号、有效期、单据、会员积分这些概念在数据库里彼此咬合。比如拆零销售同一个药品既有整盒销售又有单粒销售零售价怎么换算库存又是按哪个单位扣减这些问题如果不提前想清楚写到一半改表结构是极其痛苦的。我的一个经验是前期数据建模不要怕麻烦尽量把销售明细表和库存表设计得“冗余”一点。单据快照字段药品名称、规格、单价宁可多存也不要让历史单据去关联当前药品表库存表宁可拆到批号粒度也不要用一个总数带过。这些看似“笨”的做法恰恰是系统后续能稳定运行的关键。这个系统还有一些我很想继续扩展的方向比如对接电子处方系统处方药销售时直接拍照存档增加批发客户价格体系同一药品按客户等级给不同价格引入扫码枪和PDA做库存盘点把手持终端的扫码数据直接同步到系统里再做一套更细的销售趋势分析按药品分类、时间段、店员维度生成多维报表。如果将来门店多了还可以把单机版改造成SaaS多租户模式每个药店一套独立数据空间。最后分享一个开发技巧做这类管理系统时不要只想“单个页面怎么做”要先把“一条业务数据从录入到归档的完整生命周期”画出来。拿销售单举例它从开单、支付、扣库存、记积分、生成流水到最后进入统计报表每一个环节都对应前端一个页面、后端一个Service方法、数据库一张表的写入。三者对齐了一个页面接一个页面地写整个系统自然就顺了。如果卡在某个环节出问题优先用日志把整条链路打出来绝大部分错误都是数据流串了而不是代码逻辑不行。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑