前后端分离的那套药店管理系统,我前后折腾了两三周,从数据库建模到前端页面联调再到部署上线,中间踩了不少坑,也顺手整理了一套可以直接复用的完整源码和部署流程。这篇文章不打算讲虚的,直接把系统拆开给你看,包括为什么这么设计、核心功能怎么落地、数据库怎么建、权限怎么做、部署要注意什么,适合正在学SpringBoot和Vue整合的开发者当作练手项目参考,也适合需要快速搭一套小型进销存系统的个人开发者直接抄作业。
1. 整体设计与技术选型
1.1 为什么选前后端分离架构
传统单体项目把页面模板和业务代码揉在一起,改个小功能就要动整个项目,而药店管理系统涉及药品管理、库存预警、销售开单、会员管理等多个模块,业务规则复杂,权限粒度也细,用前后端分离的方式可以把职责彻底拆开。前端只管页面交互和数据展示,后端只负责业务逻辑和数据处理,双方通过JSON格式的接口通信,互不干扰。
这样做的好处体现在几个方面。第一是开发效率,前端可以独立并行开发,不依赖后端接口落地进度,只要提前约定好接口文档,两边各干各的就行。第二是维护成本,药店的业务规则经常变,比如新增一种处方药校验、调整库存预警阈值,后端改完接口前端基本不用动。第三是部署弹性,前端静态文件可以扔到CDN或者Nginx里,后端服务独立部署,压力大了还能单独扩容。
这套系统我选用的是SpringBoot+Vue+MyBatis+MySQL的组合。SpringBoot负责后端接口和业务逻辑,它内置Tomcat,打成一个jar包就能跑,部署起来特别省事;Vue负责前端页面和交互逻辑,配合Element UI做后台管理界面,颜值和开发效率都有保障;MyBatis负责数据库操作,SQL写起来灵活可控,适合药品这种强事务、强关联的业务场景;MySQL做数据存储,成熟稳定,药店这种数据量级的系统完全够用。
1.2 系统功能模块划分
药店管理系统从业务角度可以拆成六大核心模块,每个模块独立成功能块,又通过数据流互相串联。
药品管理是基础模块,包括药品信息的增删改查、药品分类管理、药品生产厂家管理、药品批号管理。药品管理需要额外区分处方药与非处方药,处方药在销售开单时会触发特殊的校验逻辑。
库存管理是门店运营的核心,包括入库登记、出库登记、库存查询、库存预警。库存预警非常关键,当药品库存低于预设阈值时,系统要在首页自动弹出警告,提醒运营人员及时补货。
销售管理包括前台开单、退货处理、销售记录查询。开单是整个系统操作频率最高的功能,需要支持扫码枪扫码快速添加药品、手动输入药品编号、快捷搜索药品名称,同时自动计算总金额并支持抹零操作。
会员管理包括会员信息登记、会员等级、积分累计与抵扣。药店经常搞会员日活动,会员价、积分兑换都是要支持的。
统计报表包括销售日报、月度销售额趋势图、药品销售排行、库存周转率统计。这部分数据用图表展示,方便管理者做经营决策。
系统管理包括用户管理、角色管理、菜单权限管理、操作日志管理。权限控制采用RBAC模型,不同角色看到不同的菜单和数据范围。
1.3 接口设计规范与状态码约定
前后端分离最怕接口约定不一致,前端拿不到想要的数据结构,后端不知道前端要什么格式。这套系统在接口设计上从一开始就定好了一套规范。
统一响应格式是所有接口的标配,格式如下:
{ "code": 200, "message": "操作成功", "data": { "total": 100, "list": [...] } }code为200时表示请求成功,data里是具体的业务数据;code非200时,message里是错误信息,前端拿到后直接弹Toast提示给用户看。分页接口统一使用pageNum和pageSize两个参数,返回数据里带total字段用于前端计算总页数。
接口命名统一RESTful风格,例如GET /api/medicine/list获取药品列表,POST /api/medicine/save新增药品,PUT /api/medicine/update修改药品,DELETE /api/medicine/delete/{id}删除药品。这种规范虽然前期要花时间梳理,但联调阶段省下的时间远超投入。
注意:统一响应格式一定要在项目启动时就定死,不要这个接口返回{totals:100},那个接口返回{count:100},前端写封装的时候会非常痛苦。
2. 数据库设计与核心表结构
2.1 药品表与批次表的设计思路
药品表是整个系统的核心主表,我采用的字段设计是id、medicine_name、medicine_code、category_id、specification、unit、purchase_price、sale_price、prescription_flag、manufacturer_id、status、create_time、update_time。
medicine_code是药品编码,用于扫码识别和快速检索,我建议设计成条形码格式,这样可以直接配合扫码枪使用。prescription_flag用0和1表示非处方药和处方药,这个字段在后面销售模块会作为关键判断条件。
药品批次是容易被忽略但必须单独建表的模块。药品有有效期概念,同一药品不同批次的进货价、销售价、有效期完全不同。我单独建了batch表,字段包括id、medicine_id、batch_no、stock_quantity、expire_date、purchase_price、sale_price。每个批次的库存单独记录,销售扣库存时优先扣除有效期更近的批次,这个逻辑在药店业务里是保命级的。
2.2 库存扣减与预警的数据库实现
库存不直接存在药品表里,而是通过批次表的stock_quantity字段汇总。这样每笔入库和销售都能精确到批次,效期管理才有基础。
库存预警的实现逻辑是设置一个stock_warning_threshold字段存在药品表或者批次表里,然后定期扫描数据找出当前库存低于阈值的药品。我用的是每次进入首页时后端动态计算,SQL如下:
SELECT b.medicine_id, m.medicine_name, SUM(b.stock_quantity) AS total_stock FROM batch b LEFT JOIN medicine m ON b.medicine_id = m.id WHERE b.stock_quantity < m.warning_threshold GROUP BY b.medicine_id, m.medicine_name需要注意的是,这种实时计算的方案在数据量小的时候毫无压力,但如果门店数量多、药品条目达到几万条,建议改成定时任务把预警结果写入专门的低库存表,前端直接查结果表,性能会更好。
2.3 销售单与销售明细的两级结构
销售单代码里叫order主表和order_item明细表。主表记录订单号、收银员、会员id、应付金额、实付金额、折扣金额、支付方式、销售时间;明细表记录每个药品的药品id、批次id、数量、单价、小计。
拆成两级结构有很实际的考虑。一是退货时只要针对明细表里的具体条目做操作,不用动主表数据;二是统计报表可以直接对明细表做聚合查询,计算销售排行、品类占比都很方便。订单号我建议用时间戳加随机数拼接,格式类似20240315153000123,避免用自增id直接暴露业务量。
3. 后端SpringBoot核心实现
3.1 项目分层与Maven依赖配置
后端代码按controller、service、mapper、entity、config五层组织。controller层只做参数接收和结果封装,service层承载所有业务逻辑,mapper层对接数据库查询,entity层映射数据表结构,config层放各种配置类。
Maven依赖里我重点标几个关键项。MyBatis的starter必须配合分页插件PageHelper一起使用,分页查询会非常方便;JWT认证需要引入java-jwt库,用来生成和解析登录令牌;因为是前后端分离,跨域问题要提前处理,我加了一个CorsFilter配置类来处理跨域请求。
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version> </dependency>3.2 JWT登录认证与权限拦截
用户登录成功后,后端生成一个JWT令牌返回给前端,前端把它存到localStorage里,每次请求在请求头里的Authorization字段带上这个令牌。后端写一个拦截器,从请求头里解析令牌,验证通过才放行,否则直接返回401。
JWT有一个关键点是令牌有效期,我设置的是8小时。药店员工要轮班,8小时够一个班次使用,第二天上班要重新登录,安全性有保障但又不至于频繁打扰。
为了防止同一个员工同时在多个设备登录、账号被共享使用,我在用户表里加了login_token字段,每次登录都生成一个新的UUID更新进去,同时把UUID作为JWT的一个payload字段。后台拦截器校验时发现这个UUID跟数据库里的不一致,就判定该账号在其他地方登录了,主动踢出旧登录。这个逻辑药店老板非常在意。
权限拦截后续还做了扩展,在拦截器里读取当前用户的角色标识,根据角色判断当前接口是否允许访问。角色分为系统管理员、店长、收银员三类,收银员不能访问用户管理和报表模块,店长可以查看所有经营数据但不能管理后台账号。
3.3 药品CRUD与库存事务处理
药品管理的CRUD看起来简单,但有几个细节必须处理到位。新增药品时要判断药品编码是否已存在,因为扫码枪录入了重复编码会导致后续查找药品时取到多条记录。删除药品时要先检查该药品是否存在库存批次记录,如果批次里还有库存就不能直接删除,只能标记为停用状态。
入库和销售出库必须加上事务控制。例如药店进货时,先更新或新增药品批次表,再更新药品主表的汇总库存,这两步操作任何一个失败都要回滚,否则就会出现账实不符的情况。我在service层用了@Transactional注解,同时对入库操作加了同步锁,避免并发时两个请求同时操作同一个批次导致超卖。
销售开单的核心逻辑是遍历购物车里的每个药品,逐条扣减对应批次的库存,扣减前先查一遍最新库存,判断够不够。如果遇到某个批次库存不足,就自动去查找相同药品的下一批次,按照效期从近到远依次扣减。
3.4 报表模块的SQL统计与日期处理
报表模块是老板看的最多的功能,我实现了三个重点统计。销售日报按天统计营业额和订单数,SQL用DATE_FORMAT函数把销售时间格式化为日期字符串,然后分组聚合;月度趋势是拿每天的数据画折线图;药品销售排行按销量降序排列,取前20名。
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS day, SUM(pay_amount) AS total_amount, COUNT(*) AS order_count FROM sale_order WHERE create_time >= #{startDate} AND create_time < #{endDate} GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') ORDER BY day日期范围查询有个容易踩坑的坑,数据库存的是datetime类型,前端传过来的日期只有年月日,如果直接用BETWEEN查询,会丢掉当天的最后时刻导致漏数据。我的处理方式是结束日期在查询前自动加一天,然后查询条件用>=和<来限定范围,确保把一整天的数据都覆盖到。
4. 前端Vue实现要点
4.1 路由配置与登录守卫
前端路由用Vue Router管理,我把它分成公开路由和需要登录的路由两类。登录页属于公开路由,其他所有页面都要先经过一个全局前置守卫,判断localStorage里有没有token,没有就强制跳转到登录页。
这里有个细节要处理,用户手动输入URL访问没有权限的页面时,不能只是跳登录页,还要给出友好提示。我在路由meta里面标记了每个路由需要的角色,守卫里对比当前用户的角色字段,如果不匹配就跳转到一个无权限提示页,这样比白屏或者直接404体验好得多。
4.2 Axios请求封装与拦截器
Axios封装是前后端对接的关键环节。我把请求实例单独放在一个request.js文件里,统一设置baseURL、超时时间和请求拦截器。请求拦截器里读取localStorage中的token并加到请求头的Authorization字段,响应拦截器里统一处理返回结果。
响应拦截器有两个关键判断,第一是HTTP状态码非200时直接弹出错误信息;第二是业务码code非200时,根据code值做不同处理,比如登录过期就清除本地token并跳转登录页,其他业务错误弹出message。统一封装后,每个页面里的请求代码非常干净,只关心业务逻辑。
4.3 首页数据看板与库存预警弹窗
首页是整个系统使用频率最高的页面,我设计了四个核心卡片区域展示今日销售额、今日订单数、库存预警数、会员总数,下方放近7天销售趋势图。
库存预警弹窗是首页的加分项,每次进入首页时去查询低库存药品列表,如果有数据,自动弹出对话框展示预警信息。实际使用过程中发现不能每次刷新都弹,太烦人,我改成只在登录后的第一次进入首页和手动点击刷新时才弹出。
前端图表组件我选的是ECharts。它的折线图、柱状图、饼图渲染效果很好,按需引入的方式也不会让打包体积膨胀太夸张。官方文档写得详细,遇到特殊需求也能快速找到配置示例。
4.4 销售开单页面的交互设计
销售开单页面需要重点设计,因为店员每天要在这个页面操作几十次。我把页面分成左侧的商品列表区、中间的购物车区、右侧的结算区。
商品列表区支持三种添加方式:扫码枪自动录入、输入编码后回车确认、手动搜索药品名。扫码枪本质上是键盘设备,识别到的条码会快速输入到输入框中,我在输入框上监听keyup事件,检测到回车就触发添加操作。
购物车区每一行展示药品名、规格、单价、数量、小计,数量可以加减。这里有个交互细节不能忽略,当药品数量变为0时要自动删除该行,同时总金额要实时联动更新。
结算区展示总金额和实收金额。收到现金时需要自动计算找零金额,收银员确认收款后提交订单。退货功能做成独立页面,输入原订单号可以拉出订单明细,勾选要退的商品进行退货,退货信息会写入数据库,保证原订单可追溯。
5. 部署实战与常见问题排查
5.1 前端打包与Nginx配置
前端部署前执行npm run build命令,打包产物会生成到dist目录,里面是纯静态的HTML、CSS和JavaScript文件。这些文件需要放到Nginx的html目录下,然后配置Nginx同时处理静态文件代理和API反向代理。
Nginx的核心配置文件里,把根路径指向dist目录,把/api路径代理到后端服务的地址。前端请求后端的接口统一走/api前缀,Nginx把请求转发到SpringBoot服务,这样就避免了跨域问题,因为前后端在同一个域名下。
server { listen 80; server_name localhost; location / { root /usr/share/nginx/html/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files这行非常关键,作用是当访问的路由不存在时自动回到index.html,交给前端路由接管。不配置这行,页面一刷新就404,踩过这个坑的人应该都知道有多痛苦。
5.2 后端jar包部署与配置文件调整
后端打包用mvn clean package命令,生成的可执行jar包直接通过java -jar启动。部署环境上我还会配合systemd服务管理,写一个简单的service文件,这样服务器重启后服务能自动恢复,不用每次手动启动。
配置文件application.yml里有几个部署时必须修改的点。数据库连接地址要替换成实际环境的IP和数据库名,MySQL账号密码改成高权限专用账号而不是用root,同时开启MyBatis下划线映射到驼峰属性的配置,避免实体类字段和数据库列名对不上。
还有一个容易忽略的点是文件上传路径和日志路径。项目里如果有导出报表到Excel的功能,生成的文件要写到宿主机的一个固定目录,而不要写到jar包所在的临时目录,否则服务器一重启文件就丢了。
5.3 常见问题与排查方法速查表
部署和联调过程中遇到的问题不少,我整理了一份速查表,基本覆盖了前后端分离项目最常见的坑。
| 问题现象 | 排查方向 | 解决方法 |
|---|---|---|
| 前端请求接口报跨域错误 | Nginx配置的proxy_pass是否生效 | 确认/api路径是否被正确转发,浏览器控制台看请求URL |
| 登录成功但刷新页面后跳回登录页 | token存储位置不对 | 检查localStorage里的token是否在路由守卫里正常读取 |
| 药品列表接口返回401 | JWT令牌过期或请求头未携带 | 检查是否需要重新登录,查看请求头Authorization字段 |
| 分页数据不准 | PageHelper参数传递异常 | 确认pageNum从1开始,pageSize传的是前端页面每页条数 |
| 首页预警药品不显示 | SQL数据计算逻辑问题 | 检查预警阈值字段和库存汇总查询是否正确 |
| 部署后前端图片加载404 | 静态文件路径配置错误 | 确认baseURL或静态资源路径用的是相对路径还是绝对路径 |
5.4 数据库初始化脚本与演示数据
整套系统的首次部署需要执行数据库初始化脚本,脚本里包含建库、建表、插入基础数据和演示数据。基础数据里有一个默认管理员账号,登录后可以到系统管理模块里创建其他角色的账号。
演示数据我保留了两百多条药品记录,批次数据覆盖了不同效期和时间范围,销售记录有近三个月的模拟数据。这样部署完登录进去就能直接体验各功能模块,不用反复造数据。实际部署到生产环境时,需要清掉这些演示数据再导入正式的商品目录。
6. 项目扩展方向与经验总结
6.1 可扩展的功能模块方向
这套系统目前满足了一个小型药店的基本需求,但实际业务中还有几个值得扩展的方向。对接医保支付、对接主流电商平台做线上订单接入、支持多门店库存调拨、增加药品近效期自动提醒和过期报损流程,这些都是药店真实存在的高频需求。
另一个方向是数据分析和决策支持的深化。目前报表模块停留在销售数据的聚合展示,后续可以加入毛利分析、品类结构优化建议、会员消费行为洞察和二次购买推荐。药品零售行业的竞争已经很激烈,数字化的精细化运营能力是拉开差距的关键。
6.2 我在实操中踩过的一些坑
最后分享几个实操中遇到的问题,希望能帮你少走弯路。
第一是日期跨时区问题。服务器时区如果设置的跟本地不一致,会导致后端记录的时间和前端展示的时间错位。我的做法是在数据库连接URL上强制指定serverTimezone=Asia/Shanghai,同时服务器系统时区也要同步调整。
第二是药品编码的字符集问题。药品条码里可能存在特殊字符,比如带字母和短横线,前端在搜索时如果对输入做了trim和正则校验,可能会过滤掉合法的编码请求,需要保留这些字符的兼容性。
第三是库存盘点时不能只靠系统数据。药店实操里经常有盘点盈亏,我在系统里增加了盘点单功能,操作人员录入账面库存和实盘库存,系统自动生成差异记录,并且可以一键做盘盈盘亏处理,这些差异数据不会直接修改库存,而是生成一条流水记录方便审计。
第四是并发开单场景下的优化。销售高峰期多个收银台同时开单,库存扣减如果处理不好会超卖。我在关键接口上做了Redis锁或者通过数据库行锁来控制并发,实测下来稳定性明显提升。
个人体会是,做这种管理系统,最考验人的往往不是某一项技术本身,而是把业务规则落实到代码细节里的能力。药店行业有自己的特殊性,药品效期、处方药管理、库存准确性这些环节都不能拍脑袋设计,要多去了解真实的业务场景。整套项目从零写到能稳定运行,中间有不少反复调试的过程,但跑通之后那种顺畅感是值得的。源码和部署步骤都能完整复现,希望这个项目能给你带来一些实际帮助。