
做医药管理系统这个项目我最大的感受是它跟普通的后台管理系统完全不是一回事。药品不是图书不是普通商品从入库到出库每一盒药的批号、效期、流向都得有据可查。一套真正能落地运行的医药管理系统背后的核心不是页面漂不漂亮而是数据模型能不能支撑起合规要求业务流程能不能扛住真实库房的日常操作。这次分享的项目是一套基于 SpringBoot Vue MyBatis MySQL 的企业级医药管理系统前后端分离源码完整覆盖了药品信息管理、库存出入库、采购销售、效期预警、权限管理等核心模块。适合正在做毕业设计、企业内部系统开发或者想系统学习 SpringBoot 和 Vue 全栈开发的同学参考。我会把架构设计、表结构、核心代码思路、部署过程以及我实际踩过的坑一起写出来希望你能少走一些弯路。1. 为什么做医药管理系统技术选型与整体设计思路1.1 医药管理系统的核心业务场景在动手写代码之前需要先搞清楚医药管理系统到底要管什么。和普通商品进销存相比医药行业的特殊性很突出第一是批号管理。药品入库时必须记录生产批号同一款药不同批次不能混着卖出库时要遵循先进先出原则。第二是效期管理。药品一旦过期就必须报废系统要在药品临期前自动提醒。第三是质量追溯。出了问题要能查到这批药从哪个供应商来的、发给哪个客户了。第四是GSP合规。医药经营企业需要符合药品经营质量管理规范系统操作要有记录、有复核权限要分明。这些特殊需求直接决定了数据表怎么设计、业务流程怎么走、权限怎么控制。所以做这套系统的第一个关键决策就是先把业务模型吃透再写代码而不是一上来就建表。1.2 技术栈选型为什么是 SpringBoot Vue MyBatis MySQL这个技术组合在目前的企业级管理系统开发中可以说是最主流、最稳妥的方案之一原因很直接SpringBoot简化了 Spring 的繁琐配置内嵌 Tomcat打 jar 包就能直接运行。对于中小型团队和单体系统来说开发效率和部署体验都非常好。MyBatisSQL 由开发者自己控制面对医药库存这类复杂查询场景可以精确写 SQL利用动态 SQL 灵活拼接条件性能优化空间也大。Vue组件化开发响应式数据绑定做后台管理系统的列表、表单、弹窗这类交互非常适合Element UI 组件库还能帮我们节省大量样式开发时间。MySQL开源、稳定、使用广泛配合 InnoDB 引擎支持事务保证库存扣减这类操作的原子性。这套组合的另一个好处是人才储备充足。不管是你自己后续维护还是公司招人接手会这套技术栈的人很多不会出现“项目做完了没人能维护”的尴尬局面。1.3 前后端分离架构的整体设计系统采用前后端分离架构后端提供 RESTful API前端通过 Axios 发起 HTTP 请求调用接口。开发时同时启动前端项目和后端项目前端通过代理转发请求避免跨域问题部署时后端打包成 jar 包前端打包成静态资源用 Nginx 统一托管动静分离。从功能模块上看系统划分为以下几大块模块核心功能关键点系统管理用户管理、角色管理、菜单管理基于 RBAC 的权限模型药品管理药品信息、厂家、类目、剂型药品基础档案的统一维护库存管理入库、出库、盘点、库存查询批号追踪与先进先出策略采购管理供应商、采购单、入库验收采购与库存的联动销售管理客户、销售单、销售退回处方药销售记录留存报表统计库存预警、销售统计、效期报表辅助经营决策系统监控操作日志、数据备份满足合规审计需求这里有个很重要的设计原则库存数据和业务单据数据必须分离。采购单、销售单是“流水账”库存表是“当前余量”。每次业务操作同时写单号和库存流水而不是直接改库存数量这样才能追溯每一笔变动。2. 数据层设计MySQL 表结构与 MyBatis 实战用法2.1 核心表结构设计与字段拆解表结构是整个系统的地基这里一定要花时间设计好。我实际建表时遵循了几个原则业务数据表都有主键 id、创建时间 create_time、更新时间 update_time状态字段用 int 或 tinyint不用 varchar 存中文避免后续扩展麻烦涉及金额的字段用 decimal不用 float 和 double避免精度丢失。药品主表 design 的核心字段id主键自增drug_code药品编码唯一索引drug_name药品通用名specification规格比如 0.25g*24粒dosage_form剂型片剂、胶囊、注射液manufacturer生产厂家approval_number批准文号国药准字号unit基本单位retail_price、purchase_price零售价、采购价库存表 inventory 是带有批号维度的这一点和普通库存表很不一样id主键drug_id关联药品 idbatch_no生产批号production_date生产日期expire_date有效期至quantity当前库存数量status正常、临期、过期、冻结为什么要按批号拆库存因为同一药品不同批次效期不一样先进先出必须依赖批号来实现。查询某个药品的总库存用 SUM(quantity) 按 drug_id 汇总就行出库时则要按批号逐笔扣减先出效期最近的批次。库存流水表 inventory_log 记录了每一次出入库明细id主键drug_id、batch_nochange_type入库、出库、盘点调整、报损quantity变动数量正负号区分方向ref_bill_no关联单据号比如采购单号、销售单号operate_user操作人operate_time操作时间有了流水表之后库存对账就变得很清晰。比如卖了一盒药既要在销售单明细里加一条记录又要在 inventory_log 里扣减对应的批号库存同时更新 inventory 表里的余量。这三个操作必须放在同一个事务里任何一步失败都要回滚。2.2 MyBatis 动态 SQL 在库存和查询模块的应用MyBatis 的 XML 映射文件里动态 SQL 是处理复杂查询的利器。医药管理系统里有很多“组合条件查询”的场景比如药品列表页用户可能只输入药品名可能只选择剂型也可能输入厂家加批准文号。如果每个条件都写一个方法代码会爆炸。用where加if标签可以完美解决这个问题select idselectDrugList resultTypecom.example.entity.Drug SELECT * FROM drug where if testdrugName ! null and drugName ! AND drug_name LIKE CONCAT(%, #{drugName}, %) /if if testdosageForm ! null and dosageForm ! AND dosage_form #{dosageForm} /if if testapprovalNumber ! null and approvalNumber ! AND approval_number #{approvalNumber} /if /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select药品名用 LIKE 模糊查询剂型和批准文号用等值查询MyBatis 会智能判断哪些条件需要拼接哪些条件跳过。这套写法在报表查询、销售单据筛选里同样适用写一次能复用好几年。批量插入在采购入库场景中特别常见。一张采购单可能包含几十种药品一条一条 insert 性能太差。MyBatis 支持批量插入insert idbatchInsert parameterTypelist INSERT INTO inventory_log ( drug_id, batch_no, change_type, quantity, ref_bill_no, operate_user, operate_time ) VALUES foreach collectionlist itemitem separator, ( #{item.drugId}, #{item.batchNo}, #{item.changeType}, #{item.quantity}, #{item.refBillNo}, #{item.operateUser}, #{item.operateTime} ) /foreach /insert注意 MySQL 对单条 SQL 大小有限制如果一次入库几千条数据需要分批执行每批建议控制在 500 条左右实测性能稳定。2.3 事务控制与库存扣减的一致性保证库存扣减是整套系统里最容易出 Bug 的地方因为它在并发场景下会出问题。想象一下同一个药品只剩最后一盒两个客户同时下单如果两条请求同时读到库存为 1同时执行扣减最后库存变成了负数这就是超卖。解决方案是使用数据库行级锁。在扣减库存的 SQL 里加上 FOR UPDATE让同一时间只有一个事务能操作这行数据update iddeductStock UPDATE inventory SET quantity quantity - #{quantity} WHERE drug_id #{drugId} AND batch_no #{batchNo} AND quantity #{quantity} /update这里有个关键WHERE 条件里带上 quantity #{quantity}如果受影响行数为 0说明库存不足直接抛出异常回滚事务。配合 SELECT ... FOR UPDATE 先锁定行再把流水和单据写入整个流程就是安全的。事务的边界要清晰Spring 里用 Transactional 注解标记 Service 层方法。需要注意事务只能控制由 Spring 管理的数据库连接所以不能在事务方法内自己创建新连接否则锁和回滚都会失效。这一点比较容易踩坑。3. 后端核心模块实现与 SpringBoot 配置要点3.1 用户权限与角色控制医药系统的权限控制比普通系统要求更严格。GSP 规范要求不同岗位的操作权限必须分离比如采购员不能自己审批采购单验收员和采购员不能是同一人。系统采用经典的 RBAC 权限模型共五张表。用户表 sys_user 和角色表 sys_role 是多对多关系通过 sys_user_role 关联角色和菜单的关系通过 sys_role_menu 关联。后端拦截器在每次请求时解析用户的 token拿到用户 id 后查询所有角色再查询菜单权限把权限字符集合存到本地线程变量中供后续接口校验使用。SpringBoot 里用拦截器实现统一鉴权这个方案最简单直接public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); // 解析 token获取用户信息和权限 // 校验接口权限码没有权限则返回 403 return true; } }密码处理方面一定不能用 MD5 明文加盐这种方案推荐使用 BCrypt 加密。Spring Security 框架自带 BCryptPasswordEncoder生成的结果每次都不一样而且自带盐值安全性高出很多。3.2 效期预警与批次管理的定时任务效期预警是医药系统的灵魂功能。系统里我用 SpringBoot 自带的 Scheduled 注解实现定时任务每天凌晨跑一次扫描所有库存批次Component public class ExpiryCheckTask { Scheduled(cron 0 0 2 * * ?) public void checkExpiry() { // 找出 90 天内过期的批次标记为临期 // 找出已过期的批次标记为过期并冻结销售 // 生成预警记录插入预警表 } }预警规则我做了分级距离有效期不足 90 天的批次标记为黄色预警不足 30 天标红色预警已过期的批号直接冻结不允许出库。药品列表和库存页面都会实时显示预警状态首页也放了一个临期药品的统计卡片这样库房管理员一打开系统就能看到。这里要注意一个细节定时任务默认是单线程串行执行的如果任务多了要配置线程池避免一个任务卡死影响其他任务。另外批号冻结要设计“解冻”入口因为有些药品可能延期注册或者换批号需要一个人工干预的接口。3.3 SpringBoot 配置文件的关键项我在 application.yml 里沉淀了一套比较稳妥的配置直接贴出来server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/medical?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl logging: level: com.example.mapper: debug重点说明几个关键配置serverTimezone 必须设置成 Asia/Shanghai否则日期时间查询会差 8 小时map-underscore-to-camel-case 打开后数据库的 create_time 字段能自动映射到 Java 实体类里的 createTime 属性少写很多 resultMaplog-impl 在开发阶段设为 StdOutImpl方便在控制台直接看到 SQL 执行日志上线前记得关掉这个配置否则日志量太大占磁盘。SpringBoot 版本和 MyBatis Starter 的兼容性也是一个坑。SpringBoot 3.x 的 jakarta 命名空间和 2.x 的 javax 完全不同如果你习惯看老教程直接拿 2.7 的代码往 SpringBoot 3.2 上跑大概率编译报错。如果是从零开始做项目最稳妥的选择是 SpringBoot 2.7.x MyBatis Spring Boot Starter 2.3.x资料多坑少稳定运行没问题。4. 前端 Vue 篇从环境准备到业务页面落地4.1 Vue 环境配置与工程初始化很多新手在 Vue 环境配置上就会卡住这里整理一下完整流程。先装 Node.js版本建议选 16.x 或 18.x LTS。有个容易踩的坑Node 版本太高比如 22.x老版本 Vue CLI 创建的工程可能起不来报 OpenSSL 错误。解决办法是升级构建工具版本或者直接降 Node 版本。我推荐用 Vite 而不是 Vue CLI 来初始化工程Vite 启动速度快很多对 ESM 的支持也更现代。创建工程和执行命令npm create vitelatest medical-ui -- --template vue cd medical-ui npm install npm install vue-router4 axios element-plus echarts npm run dev开发时前端端口默认 5173后端端口 8080存在跨域问题。常见做法是在项目根目录创建 vite.config.js配置代理export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })前端请求地址统一写成 /api/xxx后端 Controller 的 RequestMapping 前缀也统一是 /api。这样在开发环境走代理在生产环境用 Nginx 反代前端代码一行不用改。4.2 权限路由与菜单的动态加载医药系统的菜单权限是区分角色的。管理员能看到系统管理菜单验收员只能看到采购验收相关页面销售员看到的是销售开单页面。如果每个角色都写死一份路由表后期加一个菜单要改 n 个文件维护成本太高。更优雅的做法是动态路由。用户登录后后端返回当前用户的菜单列表前端用 router.addRoute 动态添加路由// 登录成功后获取用户菜单 const menuList await getMenuList() // 前端根据菜单数据动态注册路由 menuList.forEach(item { router.addRoute({ path: item.path, name: item.name, component: () import(/views/${item.component}.vue), meta: { title: item.title, icon: item.icon } }) })菜单存储在前端 localStorage 里刷新页面时重新加载。这里有个坑Vite 的 import 只能静态分析变量方式写 import(/views/${item.component}.vue) 偶尔会打包不出完整路由文件解决办法是把所有页面组件都手动 import 到一个 map 对象里再用 map[item.component] 取组件。如果你也想省事可以像 /router/dynamicRoutes 一样先把页面组件和路径对应关系写清楚。所有需要登录的请求在 Axios 请求拦截器里统一加上 tokenaxios.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config })响应拦截器里统一处理 401 未登录和 403 无权限跳转到登录页或提示无权限业务代码里不用到处判断登录状态。4.3 数据可视化与表单校验的细节首页的销售统计图表我用 ECharts 实现这是一个非常成熟的图表库。折线图展示近一个月的销售额走势柱状图展示药品分类销量排行饼图展示库存占比。数据来源是后端聚合查询接口一次请求返回多维统计数据前端直接填充到图表配置里。const salesChart echarts.init(document.getElementById(salesChart)) salesChart.setOption({ xAxis: { type: category, data: dateList }, yAxis: { type: value }, series: [ { name: 销售额, type: line, data: saleAmountList, smooth: true, areaStyle: { opacity: 0.15 } } ] })药品表单的校验也比一般表单复杂因为字段多、业务规则多。用 Element Plus 的 Form 组件rules 里可以配置多种验证规则。比如新增药品时批准文号必须是国药准字格式我用正则做了校验零售价必须大于采购价效期必须晚于生产日期。这些规则写在前端用户填完立刻就有反馈不用等提交到后端再报错体验好很多。Vue 组件的 computed 属性在库存统计场景特别好用。比如药品列表里要显示“可售库存”它的逻辑是总库存减去已锁定库存再减去过期数量这个计算逻辑写在 computed 里只要库存数据变化页面自动更新不用手动刷新。5. 部署打包、常见问题与避坑实录5.1 前后端构建与部署流程部署我采用的是经典的前后端分离部署方式。后端在项目根目录执行 Maven 打包命令mvn clean package -DskipTests打包完 target 目录下会生成一个 medical-system.jar。因为 SpringBoot 内置了 Tomcat所以只需要 JDK 环境上传到服务器后执行nohup java -jar medical-system.jar --spring.profiles.activeprod app.log 21 前端执行构建npm run builddist 目录下会生成静态资源文件把它上传到服务器和 Nginx 放在一起。Nginx 配置需要做两件事一是把 / 路径的请求指向前端静态资源目录二是把 /api 开头的请求反代到后端的 8080 端口server { listen 80; server_name your-domain.com; root /opt/medical/dist; index 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这个配置里有一个 Vue Router 的经典坑需要提醒如果使用 history 模式刷新页面时 Nginx 找不到路由对应的物理文件会报 404。必须在 Nginx 里加一个 fallback 配置location / { try_files $uri $uri/ /index.html; }这样所有前端路由都指向 index.html由 Vue Router 自己解析跳转。5.2 高频率报错与排查方案我把实际开发中遇到的高频问题整理成了一张速查表每个问题都是真实踩过坑后总结出来的现象原因解决方案MyBatis 一直卡在 Downloading...Maven 仓库无法访问或依赖下载超时配置阿里云镜像源检查网络代理设置前端 npm install 报 ERESOLVE 错误依赖版本冲突使用 npm install --legacy-peer-depsMyBatis 批量插入数据没写成功也不报错事务未提交或事务方法未生效检查 Transactional 是否在 public 方法上检查 SpringBoot 启动类是否扫描到了 ServiceMySQL 连接报 Public Key Retrieval 错误MySQL 8.0 认证机制变化JDBC URL 添加 allowPublicKeyRetrievaltrueuseSSLfalse中文乱码数据库或连接字符集不一致数据库表设置 utf8mb4URL 添加 characterEncodingutf8Vue 页面刷新 404前端路由 history 模式Nginx 配置 try_files 回退到 index.htmlMyBatis 批量插入不报错但数据没写进去这个坑我印象最深刻。第一次遇到时我 debug 了整整一下午发现 SQL 执行成功但数据库就是没数据。后来定位到是事务问题——Service 方法里的批量插入在没有 Transactional 的方法里默认每个 SQL 自动提交一次但循环里先插主表再插明细表每步都单独提交了最后因为外键约束导致的异常被吞掉了。所以批量操作一定要保证事务边界。还有一个 SpringBoot 版本太高的坑。如果项目用的是 SpringBoot 3.xMyBatis 的 starter 要用 mybatis-spring-boot-starter 3.0 以上版本而且所有 javax.servlet 相关的包都要改成 jakarta.servletDataSource 配置也有变化。我建议新手不要一上来就追新版本稳定运行的 2.7.x 对学习和使用都更友好。5.3 项目的扩展方向这套系统的架构后续可以继续扩展的方向非常明确药品追溯码功能。国内药品流通正在全面推进药品追溯体系建设可以在现有批号管理基础上增加药品追溯码药品电子监管码的扫码记录功能。每一盒药从出厂到销售终端每个环节扫码都有记录实现真正意义上的全程追溯。温湿度监控预警。GSP 对药品储存环境有严格要求阴凉库、冷藏库都要有温湿度监控。可以对接温湿度传感器设备定时采集数据超出范围立即报警数据留存备查。与硬件设备对接。医院或药房的自动发药机、智能货架可以通过 API 接口与系统对接扫码后自动扣减库存提高工作效率。小程序端应用。给业务员配一个移动端小程序可以在现场开销售单、查库存、扫描批号数据实时同步到主系统。这些扩展方向都是在当前架构基础上增加功能模块不用推翻重写这也是当初认真设计表结构带来的好处。5.4 开发时间线参考最后分享一个实际的项目排期参考单人或两人团队从这个系统开始到上线大概的节奏是这样的阶段周期关键交付物需求梳理与数据库设计1周表结构文档、ER 图后端基础框架与用户权限1周SpringBoot 工程、RBAC 模块药品与库存模块1周2天出入库、库存查询接口采购与销售模块1周采购单、销售单完整流程前端框架与核心页面2周登录、首页、药品管理、库存管理页面报表与预警功能3天ECharts 图表、定时任务联调与部署上线3天Nginx 部署、生产环境验证实际开发中会有很多意外情况建议多预留 30% 的时间余量。特别是前后端联调阶段接口字段对不上、时间格式不一致、分页参数不同名这些问题层出不穷。对不上的时候不要急先约定一份接口文档字段名统一会省很多事。我做这套系统最大的体会是企业级项目最关键的部分往往不是炫酷的技术而是对业务的理解深度。医药行业的批号、效期、GSP 规范这些细节才是系统真正的护城河。技术架构只是骨架业务逻辑才是血肉。如果只盯着 SpringBoot 和 Vue 怎么用而忽略了这批药从哪来到哪去那做出来的系统顶多算个玩具。希望这次的分享能帮你在做医药管理系统时少踩几个我自己踩过的坑。