尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于SpringBoot+Vue的医院药品管理系统设计与实现

基于SpringBoot+Vue的医院药品管理系统设计与实现 最近在整理自己做的几个课设项目顺手把一套相对完整的医院药品管理系统重新梳理了一遍。这个项目用的是 SpringBoot Vue 前后端分离架构后端配 MyBatis 操作 MySQL属于Java Web领域里很典型的一套组合。如果你正在找毕业设计参考或者想系统练一下主流框架的整合流程这篇文章可以把整套系统的设计思路、核心代码、数据库表结构和踩坑经历都串起来讲清楚。按照惯例先交代一下这个系统是干什么的医院药品管理说白了就是围绕药品的入库、出库、库存盘点、供应商管理、处方发药这些日常业务做的信息化工具。以往靠手工记录的方式效率低不说药品批次、有效期、库存上下限这些关键数据很容易出错所以这套系统就针对这些问题做了模块化的划分和自动化的管理。整套项目后端用SpringBoot快速搭起服务前端用Vue配合Element UI做界面MyBatis负责数据持久层的映射和灵活SQL控制MySQL作为底层存储。适合正在做JavaWeb相关设计、想搞懂前后端分离项目完整落地流程的同学直接参考。1. 整体设计与功能模块拆解1.1 系统核心需求分析动手写代码之前我习惯先把业务需求捋清楚。医院药品管理系统看似只是“增删改查”但实际做起来会发现业务链条比较长涉及的角色也不止一种。我整理出的核心角色主要有三个系统管理员负责整体基础数据维护和用户权限分配药房药师处理常规的入库、出库和盘点工作门诊医生或护士则更关注药品信息的查询和处方发药的操作。不同角色看到的功能入口和操作权限是不一样的这就要求系统在权限控制上做文章。这里我选择了基于拦截器加菜单权限控制的方案后端通过用户的角色标识来判断接口访问权限前端通过路由守卫控制页面级权限两层配合来保证操作安全。围绕这三个角色我把系统的功能模块拆成了六大块用户登录与权限管理、药品基本信息管理、药品入库管理、药品出库管理、库存盘点与预警、供应商信息管理。这六个模块基本覆盖了一个中小型医院药房日常管理的闭环。先说药品基本信息管理这里除了记录药品名称、规格、生产厂家、批准文号这些基础信息关键还在于药品分类和剂型的规范化。比如口服药、注射液、外用药品的分类字段在做统计分析时会非常有用。药品编码我建议采用规则化的方式生成比如类别缩写加序号这样后续检索和关联会轻松很多。库存管理模块是这个系统最核心的部分。入库单填写时不仅要有供货商、入库数量还要记录生产批号和有效期这两个字段直接决定了后续库存预警能不能做得准。出库时也很有意思如果同一药品存在多个批次的库存需要按有效期先后出库也就是常说的先进先出原则。这个逻辑我在后文会详细展开代码实现。库存预警是个很实用的辅助功能。我给每个药品设了库存上限和下限当库存数量低于下限或者高于上限时系统自动生成预警记录对应药品在列表里也会有醒目的颜色状态标出。用了这个功能之后缺货断档和库存积压的问题都直观了很多不用再靠人去翻账本。1.2 为什么选SpringBoot Vue这套组合技术选型这件事每个人都有自己的偏好但选SpringBoot和Vue并不单是因为热门更多是从实际开发效率和后续维护角度考虑的。后端用SpringBoot一个很重要的原因是它大大降低了配置成本。传统SSH或SSM框架的项目光配置文件就要写一大堆XMLSpringBoot通过自动配置和Starter机制把大部分重复工作都省掉了。你只需要在pom文件里引入依赖再配合application.yml里少量配置一个能跑的Web服务就出来了。这对做课设或者自己搞项目来说非常友好能把精力集中在业务逻辑而不是环境搭建上。MyBatis之所以被我放在这里用主要是看重它灵活的半自动SQL管理方式。虽然JPA也很强大但在处理复杂查询、多表联查和动态SQL场景下MyBatis的SQL控制粒度更细排查问题时也更容易定位。比如药品入库单查询需要关联药品表、供应商表、操作员表用MyBatis的resultMap做结果映射整个过程一目了然。而且网上关于MyBatis的资料非常多遇到问题能快速找到解决方案。前端选Vue则是看中它的组件化开发模式和生态。Vue的双向数据绑定让表单交互写起来非常顺滑Element UI组库提供的表格、弹窗、表单校验组件能直接满足管理系统的界面需求不需要重复造轮子。再加上Vue Router和Axios的配合页面路由和接口请求的整合也很顺手。整体来说这套技术栈的选择是“在合适的场景用合适的工具”而不是单纯的跟风。1.3 项目整体架构与目录规划后端项目的包结构我采用的是常规的分层模式。Controller层负责接收前端请求和参数校验Service层承担业务逻辑处理Mapper层通过接口定义加XML文件的方式完成数据库操作实体类对应数据库表结构。再加一个common包存放统一返回结果、异常处理器和工具类config包放拦截器和CORS配置。com.hospital.drug ├── controller │ ├── DrugController.java │ ├── StockInController.java │ ├── StockOutController.java │ ├── SupplierController.java │ └── UserController.java ├── service │ ├── DrugService.java │ ├── StockService.java │ └── ... ├── mapper │ ├── DrugMapper.java │ ├── DrugMapper.xml │ └── ... ├── entity │ ├── Drug.java │ ├── StockIn.java │ ├── StockOut.java │ └── ... ├── common │ ├── Result.java │ ├── GlobalExceptionHandler.java │ └── ... └── config ├── WebConfig.java └── ...前端目录同样按模块划分views下面按功能页面建目录router里集中配置路由规则api目录统一封装请求方法store里用Vuex管理登录状态和全局信息。这种结构虽然初期会多花一点时间搭框架但后面每加一个功能都只需要按照既定模式往里面填代码维护成本低很多。2. 数据库设计思路与核心表结构2.1 关键表设计与字段规划数据库设计是整个系统的地基表结构设计得好不好直接影响后续开发的复杂度。我最先规划的是用户表字段包括用户ID、用户名、密码、姓名、角色、联系电话、创建时间。密码这里我用的是MD5加密存储虽然现在更推荐BCrypt但作为课设演示MD5已经够用如果后续要上生产环境建议换成BCrypt。药品表是业务核心字段设计上我重点关注的是药品编码、药品名称、通用名、规格、生产厂家、批准文号、剂型、药品分类、单位、库存数量、库存上限、库存下限、有效期、生产批号、创建时间和更新时间。其中库存数量这个字段我建议不要叫stock直接叫stockNum避免和数据库关键字冲突少踩不必要的坑。入库表和出库表采用主从表设计思路。以入库为例入库主表记录本次入库单号、供应商ID、操作人ID、入库日期、备注入库明细表则记录该单号下的每个药品、数量、单价、生产批号、有效期。为什么要分两张表因为一次入库通常包含多个药品如果全部塞进一张表字段冗余会非常严重而且统计总金额时会很痛苦。CREATE TABLE drug ( id INT PRIMARY KEY AUTO_INCREMENT, drug_code VARCHAR(50) NOT NULL, drug_name VARCHAR(100) NOT NULL, generic_name VARCHAR(100), specification VARCHAR(100), manufacturer VARCHAR(200), approval_number VARCHAR(100), dosage_form VARCHAR(50), drug_category VARCHAR(50), unit VARCHAR(20), stock_num INT DEFAULT 0, stock_min INT DEFAULT 10, stock_max INT DEFAULT 500, create_time DATETIME, update_time DATETIME );2.2 表关系梳理与索引优化表之间的关系不算复杂但需要理清楚。用户表和入库表是一对多的关系一个用户可以操作多次入库供应商表和入库表也是类似的关系药品表和入库明细表是多对多的关系通过入库明细表关联。这里我在明细表里冗余了药品名称字段用空间换查询效率避免每次查明细都要JOIN药品表。索引方面除了主键索引之外我建议在药品表的drug_code上建唯一索引保证编码不重复在入库主表的order_no上建唯一索引在药品表的drug_name上建普通索引方便按名称进行模糊查询。经过实测当药品数据量在几万条级别时加索引和不加索引的查询速度差距会非常明显后者可能出现几百毫秒甚至秒级的响应延迟。3. SpringBoot后端核心功能实现3.1 项目初始化与必备依赖配置创建一个SpringBoot项目我习惯直接用IDE的Spring Initializr生成初始骨架。Java版本我用的JDK 1.8SpringBoot版本选择2.7.x系列不要一上来就追最新版很多第三方依赖还没适配新版SpringBoot排查起来费时费力。pom.xml中核心依赖如下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesapplication.yml配置中需要注意几个点。数据库连接串要加上useSSLfalse和serverTimezoneAsia/Shanghai参数否则会报SSL连接警告或者时区错误。MyBatis的mapper-locations配置为classpath:mapper/*.xml这样XML文件才能被正确扫描到。还有一个很实用的小配置就是map-underscore-to-camel-case设为true可以自动将数据库的下划线字段映射为Java的驼峰属性少写很多转义代码。这里是配置片段spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/hospital_drug?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 username: root password: 123456 type: com.alibaba.druid.pool.DruidDataSource mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl最后一行log-impl配置也很常用开发阶段可以打印执行的SQL语句排查问题的效率能提高不少。3.2 用户登录与权限拦截实现登录接口的逻辑并不复杂前端传来用户名和密码后端根据用户名查询用户信息比对MD5加密后的密码是否一致一致就返回用户信息和token。这里的token我采用了简单的方式用UUID生成一个随机字符串存放在Redis里每次请求携带token时到Redis里验证用户身份和权限。如果你不想引入Redis也可以直接用拦截器校验登录状态但token校验建议还是做一下否则接口裸奔在公网上会很不安全。拦截器的实现方式很直接继承HandlerInterceptor接口在preHandle方法里从请求头获取token如果为空或校验失败就直接返回401状态码和提示信息。然后把WebMvcConfigurer里注册拦截器并配置好放行路径比如登录接口和静态资源需要放行其余接口全部拦截。Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(token); if (StringUtils.isBlank(token)) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录或登录已过期\}); return false; } // 校验token并存入ThreadLocal return true; }3.3 药品入库与出库的核心事务处理入库和出库看起来只是对库存字段做加减操作但实际上必须保证数据一致性。以入库为例流程是这样的前端提交入库单主表信息和药品明细数据列表后端收到请求后先做参数校验再通过事务机制保证主表和明细表的数据同时写入最后更新药品表里的库存数量。这个场景必须加上Transactional注解。原因很直接如果入库主表写入成功但明细表写入失败或者库存更新失败整个操作就会出现数据不一致的情况。通过事务回滚一个环节出错就把所有操作都撤销保证数据库永远处于一个完整的状态。入库的核心代码逻辑如下Transactional(rollbackFor Exception.class) public void addStockIn(StockInDTO dto) { StockIn stockIn new StockIn(); stockIn.setOrderNo(generateOrderNo()); stockIn.setSupplierId(dto.getSupplierId()); stockIn.setOperatorId(dto.getOperatorId()); stockIn.setInDate(new Date()); stockInMapper.insert(stockIn); int orderId stockIn.getId(); for (StockInDetail detail : dto.getDetails()) { detail.setOrderId(orderId); stockInDetailMapper.insert(detail); // 更新药品库存 drugMapper.increaseStock(detail.getDrugId(), detail.getQuantity()); } }出库的逻辑和入库类似但多了一个批次选择的处理。因为先进先出原则出库时需要先查这个药品有哪些批次的库存超过有效期的按有效期升序排列优先出库最早到期的批次。这块我单独写了一个批次查询方法然后按顺序扣减库存。3.4 MyBatis动态SQL与多表查询应用MyBatis最让我喜欢的就是动态SQL的能力。以药品列表的模糊搜索为例搜索条件可能有药品名称、药品分类、生产厂家而且还支持组合查询。用动态SQL可以根据条件是否为空动态拼接SQL片段不需要写多个不同方法。select idfindDrugList resultTypecom.hospital.drug.entity.Drug SELECT * FROM drug where if testdrugName ! null and drugName ! AND drug_name LIKE CONCAT(%, #{drugName}, %) /if if testdrugCategory ! null and drugCategory ! AND drug_category #{drugCategory} /if if testmanufacturer ! null and manufacturer ! AND manufacturer LIKE CONCAT(%, #{manufacturer}, %) /if /where ORDER BY id DESC /select使用 标签的好处是如果所有条件都为空查询就退化成普通的全表查询不用担心多余AND导致的SQL语法错误。这个细节在实际开发中非常实用尤其在写后台管理列表时搜索条件经常会有很多可选项。4. Vue前端页面设计与交互实现4.1 Vue项目初始化与环境配置Vue项目我使用Vue CLI创建版本选择Vue 2.6.x。为什么不用Vue 3因为Element UI对Vue 2的支持更成熟稳定而且网上的例子和踩坑记录更多适合做课设时快速上手。如果你有时间研究Vue 3加Element Plus代码逻辑基本通用差异主要在于组件引入方式和部分API写法。创建项目的命令很简单npm install -g vue/cli vue create hospital-drug-front创建完成后需要安装几个核心依赖包括vue-router、axios、element-ui、vuex。安装Element UI时要注意使用完整引入还是按需引入课设阶段我推荐完整引入省事如果是企业级项目按需引入可以减少打包体积是一个值得优化的方向。npm install vue-router3 axios element-ui vuex3axios封装这块我建议单独建一个request.js文件在拦截器里统一添加token到请求头并处理响应状态码。比如后端返回401时前端自动跳转到登录页返回500时统一弹出错误提示。这样业务代码里就不用每个请求都重复写错误处理逻辑了。4.2 前端路由与左侧菜单的权限控制前端页面结构是经典的布局模式左侧是菜单栏顶部是用户信息和退出按钮中间是主要内容区域。路由设计分为两层外层是Layout布局组件内层才是具体的功能页面。每个菜单项都对应一个路由点击菜单时通过router.push跳转到对应页面。权限控制在路由层做了守卫拦截。在router.beforeEach中判断当前用户是否已登录未登录则跳转到登录页。对于不同角色的访问控制我采用了一个比较简化的方案菜单项的生成通过后端返回的菜单JSON数据动态渲染普通员工登录后只显示自己有权限访问的菜单。router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path /login) { next(); } else { if (token) { next(); } else { next(/login); } } });4.3 药品列表页面的表格与搜索实现药品列表页面是整个前端最典型的页面包含搜索表单、操作按钮、数据表格和分页组件。页面的关键逻辑是加载数据时调用后端接口API把搜索条件和当前页码、每页条数作为请求参数传过去接收返回的数据列表和总条数然后渲染到表格中。搜索表单我使用了Element UI的el-form组件表单项横向排列包括药品名称输入框、药品分类下拉框、生产厂家输入框以及查询和重置按钮。用户点击查询时重置页码为1并触发数据加载点击重置时清空所有搜索条件后重新加载。这一套交互逻辑在后台管理系统里非常常见写熟之后几乎可以复制到所有列表页面。表格部分是el-table配合el-table-column字段包括药品编码、名称、规格、生产厂家、剂型、库存数量、库存上下限、状态标签和操作列。状态标签这里我做了个小优化通过计算属性的方式判断库存状态低于下限显示红色标签“库存不足”高于上限显示黄色标签“库存过多”正常则显示绿色标签“正常”。这个小功能视觉反馈很直观用户看一眼列表就能发现异常药品。分页用的el-pagination组件设置好current-page、page-size、total和对应的事件后端接口返回的total字段就是分页总条数。需要提醒的是el-pagination的current-page和page-size要使用.sync修饰符或者在事件里手动赋值否则会出现翻页不生效的问题。4.4 入库单填写页面的动态表单实现入库单填写页面比较有代表性因为它包含一对多的表单结构也就是一个主表信息加多条明细数据。主表信息包括供应商和入库日期明细部分是一个可动态增删的表格每行可以填写药品名称、入库数量、单价、生产批号和有效期。动态表单的实现思路是维护一个明细数组点击添加按钮时向数组push一个空对象每行的药品名称用el-select的下拉选择器通过远程搜索的方式从后端接口模糊匹配药品填完一行再添加下一行。删除按钮对应从数组里移除当前索引的对象。提交时把主表信息和明细数组组装成一个JSON对象通过接口发送给后端。前端这里我做了一个校验逻辑至少添加一条明细才能提交每条明细的药品、数量、单价都不能为空否则给出提示。这样可以在源头上减少后端的无效请求。5. 从零运行整套系统的完整步骤5.1 数据库初始化与基础数据导入拿到项目源码后第一步是把数据库跑起来。我提供的SQL文件里包含了建库、建表和基础数据导入语句直接用Navicat或者MySQL命令行执行即可。mysql -u root -p hospital_drug.sql执行完以后进入到数据库里看一下应该能看到user、drug、stock_in等相关表。这里插入一条管理员账号初始密码是123456登录之后可以自行修改。需要注意如果你本机MySQL的密码不是123456记得同步修改后端application.yml里的数据库密码否则系统启动时会连不上数据库。5.2 后端启动与接口自测后端项目导入IDE后等待Maven下载完成依赖然后在Application类上右键运行即可。启动成功的标志是控制台出现SpringBoot的启动日志和端口号。默认端口我设置的是8080如果被占用可以调整application.yml里的server.port。接口自测建议使用Postman先请求登录接口拿token然后在请求头里带上token测试药品列表查询、新增药品、入库、出库等核心接口。这里有一个测试技巧先用浏览器直接访问后端的Swagger地址如果集成了Swagger那个界面里可以直观看到所有接口定义和参数格式调试效率比Postman更高。5.3 前端启动与功能联调前端项目导入IDE后先执行npm install安装依赖这一步耗时取决于网络环境最好用国内镜像源。安装完成后执行npm run serve启动开发服务器默认地址是localhost:8081和后端的8080端口就区分开了。前后端联调时最常遇到的就是跨域问题。我在后端WebConfig里配置了CORS跨域支持允许前端地址的跨域请求所以正常情况下不用额外处理。如果前端仍然报跨域错误检查一下axios的baseURL是否写对以及后端CORS配置是否被其他拦截器拦截了。6. 常见问题与排查技巧实录6.1 数据库连接报错和时区问题这是我遇到最多的一个坑。MySQL 8.x版本连接时如果不指定serverTimezone参数经常会报Server returns invalid timezone的错误。解决方式很直接在JDBC连接串中加上serverTimezoneAsia/Shanghai。还有SSL警告虽然不影响运行但看着难受也建议加上useSSLfalse。另外还有字符集问题如果连接串没加characterEncodingutf8前端传过来的中文可能乱码。这个在数据库创建时就建议统一utf8mb4字符集同时连接串和表字段都保持utf8相关配置一致。6.2 MyBatis初始化失败的应对方法MyBatis启动时报Invalid bound statement错误通常是因为Mapper接口和XML文件没有正确对应。检查三个地方接口名和XML命名空间是否一致方法名和XML里的id是否一致application.yml里的mapper-locations是否指向了XML文件所在的目录。如果配置都没问题可以留意一下target目录有时候Maven清理后没有重新编译XML文件。解决办法是在pom.xml中配置resources节点明确把src/main/resources下的xml文件包含进构建产物中避免这类坑。6.3 前端常见报错与调试思路前端启动时报Module not found或者依赖缺失绝大多数是npm安装依赖时出了问题。删除node_modules目录后重新安装一遍或者检查package.json里是否缺了依赖。如果是在CodeSandbox这类在线环境跑项目安装依赖速度慢还可能引发超时。页面数据加载不出来时打开浏览器开发者工具的Network面板看请求是否发出、状态码是多少、返回的JSON结构是否符合预期。掌握了这个排查思路90%的前后端联调问题都能快速定位到根源。7. 项目扩展方向与个人体会系统运行稳定之后可以考虑往几个方向做扩展。比如引入Spring Security做更精细的权限控制把基于拦截器的简单认证替换为更完善的认证授权体系。也可以增加ECharts图表做药品消耗趋势统计直观展示各科室用药量变化。还可以把Excel导入导出功能加上让药品批量导入和报表导出的效率大大提升。最后分享一个我在实际项目中养成的习惯每次开发新功能前先把数据库表和接口文档列出来再开始写代码。虽然会多花一点时间但后面联调和调试的效率会高非常多算是用最小的成本换最大的确定性。这套系统的源码结构和代码风格我都做了相应的整理和标注注释写到了关键业务逻辑的位置希望能给正在做类似项目的同学一点参考。遇到具体问题时可以从报错信息入手把日志和响应内容发出来多对比几次大多数问题都能找到规律。
返回列表