
简介这是一套面向高校毕业设计及课程设计的医院药品管理系统完整项目基于SpringBoot开发后端服务搭配Vue构建前端界面。功能模块覆盖用户注册登录、角色权限控制、药品信息与分类管理、实时库存监控、采购申请与审批流程贴合医院药品管理常见业务场景可作为计算机专业学生毕业设计或期末项目的参考模板。资源压缩包共415个文件整体约24.08MB主要包含Java源码、Vue组件、SVG图标、XML配置等开发文件同时提供SQL数据库脚本、bat启动脚本和演示视频方便直接导入数据库、运行前后端并查看操作流程。已有59人学习。借助这份资料既能学习SpringBootVue的模块化编码与前后端交互也能获得论文撰写时的功能划分思路、数据库设计方法和项目演示素材是比较完整的毕设解决方案。1. 医院药品管理系统趁早跑通这套 SpringBoot Vue 闭环别让答辩卡在“看不懂项目”上拿到这套基于 SpringBoot 的医院药品管理系统里面装的是 Vue 打包后的前端资源和一套 Java 后端代码。对正在做课程设计或者毕业论文的 Java 开发者来说这套东西最值钱的地方不是登录注册那点代码而是把药品信息、库存监控、采购审批串成了一条完整的业务闭环药剂师提交采购申请 → 管理员审核 → 审核通过生成采购订单 → 入库回填库存 → 库存低于阈值自动触发补货提醒。这条闭环链在大部分毕设项目里是断的而答辩时老师恰恰最爱顺着这条链追问“库存扣减怎么防负数”“报警逻辑在哪儿触发”。跑通它、看懂它你就已经赢了一半。第 1 章先点到这一步。接下来我会按“先部署跑通 → 权限认证 → 库存闭环 → 踩坑记录 → 增强方案”的顺序把这套系统的落地细节全部抠出来。2. 先跑通再改代码前端构建产物、三个批处理脚本和 Maven 生命周期2.1 Vue 打包产物和 SpringBoot 的静态资源映射你先看这套资源里的前端文件index.html、app.c300fcf6.css、chunk-vendors.a72b0961.css。文件名带 hash说明这是 Vue 项目执行npm run build之后生成的 dist 目录内容。Vue 开发环境的publicPath一般配成/打包后index.html里的script src/js/app.xxx.js这类路径就是绝对路径必须按这个路径放到 SpringBoot 能访问到的地方。SpringBoot 的静态资源默认目录是classpath:/static/你项目里src/main/resources/static下的所有文件都会被映射到根路径/。也就是说你访问http://localhost:8080/时后端发现没有对应的 Controller就会自动去 static 目录查找index.html直接把页面返回给浏览器。这就是为什么这套资源没有把前端单独部署到 Nginx而是直接塞进后端打成一个 jar 包——前后端同源就彻底绕开了跨域问题。实际操作时你把index.html和static或 js/css 目录对应的文件原样拷进src/main/resources/static不要改文件名。注意 hash 文件名是 chunk 引用关系app.c300fcf6.css被index.html里的link标签引用你一旦重命名页面就会失去样式。我见过有人为了“清爽”把带 hash 的文件改成 app.css结果页面裸奔。2.2 1-install.bat、2-run.bat、3-build.bat 各自干了什么这套资源里带了三个批处理脚本名字就标了顺序。先看大概职能脚本常见内容对应生命周期1-install.batmvn clean install把项目依赖打到你本地 Maven 仓库编译、测试、打包2-run.batjava -jar target/xxx.jar或mvn spring-boot:run启动应用3-build.bat重新打包后端或者包含npm install npm run build重新构建产物典型的 1-install.bat 内容长这样echo off echo 开始构建项目 mvn clean install -DskipTests echo 构建完成 pause逻辑说明mvn clean install会先清空 target 目录编译源码执行单元测试这里用-DskipTests跳过测试以免环境问题导致中断最后把打好的 jar 包安装到本地仓库。pause是为了让你看到构建结果不然窗口一闪就关了。2-run.bat 更像是这样echo off set JAVA_HOMED:\Java\jdk1.8 set PATH%JAVA_HOME%\bin;%PATH% java -jar target\hospital-manage-1.0.0.jar依赖 1-install.bat 已经把 jar 打出来了run 脚本就只负责启动。这儿有个关键点脚本里如果硬编码了set JAVA_HOMED:\Java\jdk1.8你机器上 JDK 装在别的位置直接双击就会报找不到java命令或者启动失败。我建议你先打开 cmd手动java -version确认环境变量通不通再执行脚本。3-build.bat 大概率用于重新打包如果你拿到了 Vue 源码脚本里会先把前端构建出来再回到后端目录执行mvn package。如果这套资源只给了打包好的 dist那 3-build.bat 一般就是后端重新打包。你在改完后端代码后跑一次它就能生成新的 jar不用每次都手动敲mvn package。2.3 数据库连接与端口的最小改动跑通之前必须把application.yml里的数据库配置改对。常见的配置如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/hospital_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.hospital.entity逻辑说明url里hospital_db是数据库名必须用资源附带的 SQL 脚本先创建库不然启动时数据源初始化直接报错。serverTimezoneAsia/Shanghai解决 MySQL 8 的时区问题如果你用的是 MySQL 5.7serverTimezone可以保留也可以去掉。driver-class-name这里写的是com.mysql.cj.jdbc.DriverMySQL 8 以后必须用这个新的驱动类名旧版的com.mysql.jdbc.Driver在 MySQL 8 上会抛异常。端口这里要强调一下8080 是 SpringBoot 的默认端口。你机器上如果已经跑了其他服务占用了 8080启动会直接失败控制台报Port 8080 was already in use。不改代码直接启动参数指定java -jar target/hospital-manage-1.0.0.jar --server.port8081改成 8081 后前端index.html里如果调用的接口路径是写死的http://localhost:8080/api/...那就会出现页面能打开但接口全部报错的情况。前端一般会把 baseURL 封装成一个 axios 实例你找到那个 JS 文件里baseURL常量改掉或者全局搜8080批量替换。提示改动 application.yml 后如果用了 IDE 直接启动检查一下是否自动加载了application-dev.yml或application-prod.yml这种 profile 配置经常会发现你改了主配置文件实际生效的是另一个。3. 登录认证与角色权限拦截器加注解的权限模型3.1 用户表设计和登录接口处理从摘要描述来看系统有药剂师、医生、管理员三种角色登录后必须拿到角色信息后面所有接口都要根据角色判断有没有权限操作。用户表一般是这种结构CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT MD5或BCrypt密文, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, role varchar(20) NOT NULL COMMENT admin/pharmacist/doctor, status tinyint(4) DEFAULT 1 COMMENT 1启用 0禁用, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;密码这块毕设项目里 90% 都是 MD5 处理DigestUtils.md5DigestAsHex(password.getBytes())即可。但你如果用 BCrypt答辩证时反而能多说一层“MD5 可被彩虹表破解BCrypt 自动加盐”。后端登录接口通常长这样RestController RequestMapping(/api/user) public class UserController { Autowired private UserService userService; PostMapping(/login) public ResultUser login(RequestBody LoginRequest request) { User user userService.login(request.getUsername(), request.getPassword()); return Result.ok(user); } }Service 层里干的事是查用户 → 校验状态 → 比对密码 → 把用户对象返回给前端。如果密码是 MD5比对时要把输入的密码做一次 MD5 再和库里密文比较。登录成功怎么维持会话毕设里两种做法都有Session 方式登录成功把 user 对象塞进request.getSession().setAttribute(loginUser, user)后续接口从 session 取。Token 方式登录成功生成一个随机 UUID 作为 token存 Redis 或内存 Map响应给前端前端每次请求在 Header 里带上Authorization: token值。Token 方式更接近企业实际开发答辩分数也更高。如果你拿到的版本是 Session 的想改成 Token 也不难后面避坑章节我会说具体改法。3.2 拦截器实现登录校验和角色控制角色权限不能靠前端控制别人绕过页面直接调接口照样能删数据。后端必须守好接口。SpringBoot 里最常见的做法是写一个HandlerInterceptor拦截器注册进来统一校验。Component public class AuthInterceptor implements HandlerInterceptor { Autowired private TokenManager tokenManager; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行非Controller请求比如静态资源文件 if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod (HandlerMethod) handler; // 方法上标注了 NoAuth 的接口直接放行比如登录接口 if (handlerMethod.getMethodAnnotation(NoAuth.class) ! null) { return true; } String token request.getHeader(Authorization); if (token null || token.isEmpty()) { writeUnauthorized(response, 未登录或token为空); return false; } // 从token解析出用户 User user tokenManager.getUserByToken(token); if (user null) { writeUnauthorized(response, token无效或已过期); return false; } // 把当前用户放到request作用域后续Controller里用 RequestAttribute 获取 request.setAttribute(currentUser, user); // 检查角色权限 RoleRequired roleRequired handlerMethod.getMethodAnnotation(RoleRequired.class); if (roleRequired ! null) { String[] allowedRoles roleRequired.value(); boolean allowed Arrays.asList(allowedRoles).contains(user.getRole()); if (!allowed) { writeUnauthorized(response, 权限不足); return false; } } return true; } }逻辑说明preHandle要返回false才拦截请求。第一步先判断是不是HandlerMethod拦截器会拦到静态资源请求比如 CSS、JS、图片这些请求的 handler 不是方法调用而是ResourceHttpRequestHandler如果不判空直接强转就会白屏这是拦截器一个经典坑。第二步检查方法上有没有自定义的NoAuth注解登录接口必须放行。第三步从 Header 拿 token拿不到直接返回 401。第四步把用户放到 request 里贯穿请求。第五步根据方法上的RoleRequired注解做角色判断。角色注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RoleRequired { String[] value(); }用法就很好看了RoleRequired({admin, pharmacist}) GetMapping(/drug/list) public ResultListDrug list() { ... }只有管理员和药剂师能访问药品列表医生访问就返回权限不足。这个模型写进论文里可以画一张表列出来“哪些接口允许哪些角色访问”显得你确实设计了权限体系而不是写死。最后注册拦截器Configuration public class WebMvcConfig implements WebMvcConfigurer { Autowired private AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/user/login, /api/user/register); } }逻辑说明addPathPatterns(/api/**)表示只拦 api 路径下的接口静态资源不拦。excludePathPatterns放行登录注册。这里有个常见失误如果你把放行路径写错比如登录接口实际是/api/user/login你只排除了/user/login那拦截器照样拦住登录请求前端调登录接口直接 401但页面上看又是能正常打开的排查起来很绕。3.3 给角色加权限的通用做法有些毕设需求对权限更细化比如医生只能查看药品不能修改价格药剂师可以录药品但不能审批采购单。这种场景下拦截器里硬编码角色判断会很难维护。常见做法是引入权限码概念建一张权限表用户登录时查出所有权限码放进 session每次接口校验方法上的RequirePermission(drug:delete)是否存在。改法不复杂核心就是登录查询多带一步多表关联public SetString getUserPermissions(Long userId) { return userMapper.selectPermissionsByUserId(userId); }然后拦截器校验逻辑变成SetString permissions (SetString) request.getSession().getAttribute(permissions); if (requiredPermission ! null !permissions.contains(requiredPermission)) { // 无权限 ... }好处是权限配置挪到数据库管理员界面可以动态分配角色权限而不是改代码重新部署。答辩时这是很好的“扩展性”论点。缺点就是多了一堆表和关联查询如果你只求快速交付拦截器加注解已经够用了。4. 库存管理闭环实时监控、出入库记账和采购流程衔接4.1 库存表设计与出入库扣减逻辑库存监控的前提是表结构得能支撑查询。常见设计是三张表drug药品主表id、药品编码、名称、规格、生产厂家、分类、有效期、批准文号。drug_stock库存表id、drug_id、当前库存 quantity、预警阈值 min_stock、锁定数量 lock_stock。stock_record流水表id、drug_id、类型IN 入库 / OUT 出库 / LOCK 锁定、数量、操作人、关联采购单号、时间。为什么库存单拆一张表而不是把数量直接写在 drug 表上因为出库入库要记流水每次变动都得有据可查账实相符是药房管理的基本要求。你论文里把三张表的 ER 图画出来比只画一张 drug 表信息量大得多。出库扣减是库存系统最核心的代码直接决定会不会出现负数。我一般这样写 ServiceService public class StockService { Autowired private DrugStockMapper stockMapper; Autowired private StockRecordMapper recordMapper; Transactional(rollbackFor Exception.class) public void stockOut(StockRecord record) { // 1. 扣减库存SQL带数量条件返回受影响行数 int rows stockMapper.reduceStock(record.getDrugId(), record.getQuantity()); if (rows 0) { throw new RuntimeException(库存不足或药品不存在扣减失败); } // 2. 插出入库流水 record.setType(OUT); recordMapper.insert(record); } }Mapper 里的 SQL 是关键update idreduceStock UPDATE drug_stock SET quantity quantity - #{quantity}, update_time NOW() WHERE drug_id #{drugId} AND quantity #{quantity} /update逻辑说明UPDATE语句加了quantity #{quantity}这个条件后MySQL 执行时会先判断当前库存是否足够不够就更新不了影响行数为 0。这个方案天然防止库存扣成负数而且不用手动SELECT再比较避免了并发下两个请求同时读到库存充足都去扣减的问题。有人会问我先把库存查出来if (stock.getQuantity() outNum)不够就抛异常不行吗行但高并发下不行。两个请求同时读到 quantity15一个出库 12 一个出库 5各自校验都通过最终库存变成 -2。而 SQL 条件更新在数据库层面是原子的同一个 drug_id 的更新会自动排队第二个请求执行时 quantity 已经变成 3quantity 5为 false行数为 0扣减失败。入库逻辑对应加库存Transactional(rollbackFor Exception.class) public void stockIn(StockRecord record) { int rows stockMapper.increaseStock(record.getDrugId(), record.getQuantity()); if (rows 0) { // 库存表中没有这条药品记录先插入 stockMapper.initStock(record.getDrugId(), record.getQuantity()); } record.setType(IN); recordMapper.insert(record); }increaseStock就是UPDATE drug_stock SET quantity quantity #{quantity} WHERE drug_id #{drugId}加库存不会出现负数问题但要注意首次入库时库存表可能没有记录所以先更新再判断行数为 0 再初始化一条新记录。4.2 实时库存监控和预警实现摘要里说“系统实时监测药品库存情况并提供库存报警功能提醒补货”落地的常见方案有两种方案一是查询列表的时候带上库存状态。把药品列表和库存做关联查询select idselectDrugWithStock resultTypecom.hospital.vo.DrugStockVO SELECT d.id, d.name, d.spec, d.manufacturer, s.quantity, s.min_stock, CASE WHEN s.quantity s.min_stock THEN 不足 WHEN s.quantity s.min_stock * 1.5 THEN 偏低 ELSE 正常 END AS stock_status FROM drug d LEFT JOIN drug_stock s ON d.id s.drug_id /select逻辑说明这里用CASE WHEN在 SQL 里直接算库存状态返回给前端展示红色/黄色标签。阈值写了两档低于min_stock是“不足”低于 1.5 倍是“偏低”这样避免刚低于预警线就反复提醒给采购留一个缓冲期。方案二是定时任务扫描报警。后面第六章我会专门给一个完整写法这里先提一句报警不只是页面变色最好在系统登录后的首页有个未处理采购提醒点击直达缺货药品清单这才是闭环。4.3 采购申请与审批的状态流转采购流程在代码里其实就是一张purchase_order表加状态字段CREATE TABLE purchase_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) DEFAULT NULL COMMENT 采购单号, drug_id bigint(20) DEFAULT NULL, quantity int(11) DEFAULT NULL, supplier varchar(100) DEFAULT NULL COMMENT 供货商, status tinyint(4) DEFAULT 0 COMMENT 0待审核 1已通过 2已拒绝, apply_user varchar(20) DEFAULT NULL COMMENT 申请人, audit_user varchar(20) DEFAULT NULL COMMENT 审批人, apply_time datetime DEFAULT NULL, audit_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;药剂师提交采购申请后管理员审核接口改变状态PostMapping(/purchase/audit) RoleRequired({admin}) public ResultVoid audit(RequestBody AuditRequest request) { PurchaseOrder order orderService.getById(request.getOrderId()); if (order null || order.getStatus() ! 0) { return Result.error(采购单不存在或已处理); } order.setStatus(request.getPass() ? 1 : 2); order.setAuditUser(LoginUserUtil.getCurrentUser().getUsername()); order.setAuditTime(new Date()); orderService.updateById(order); // 如果审核通过可以直接触发生成一笔入库预期记录 if (request.getPass()) { stockService.prepareInbound(order); } return Result.ok(); }逻辑说明RoleRequired({admin})保证只有管理员能调这个接口。状态机是“待审核 → 已通过/已拒绝”一旦处理过就不能重复审批。审核通过后调用prepareInbound可以在库存表插入一条锁定数量等待真正入库时再转为实际库存这样就能做到账实相符。5. 避坑记录5 个跑不起来或跑起来不对的经典问题这套系统我前后跑过两遍每一遍都在不同环节翻过车。下面按“现象 → 原因 → 解决”把问题写清楚你再遇到就能直接对号入座。5.1 双击 2-run.bat 黑窗口一闪而过现象双击批处理文件后窗口刚出现就消失完全看不到日志。原因两种情况最常见。第一JDK 环境变量没配好系统找不到java命令第二脚本里写的JAVA_HOME是作者的机器路径好比写死D:\Java\jdk1.8你的 JDK 装在 C 盘脚本就崩了。解决不要直接双击。先打开 cmdcd 到项目目录手动执行java -version确认 JDK 正常再执行java -jar target\hospital-manage-1.0.0.jar看完整日志。如果提示Error: Could not find or load main class说明 jar 没打出来先跑 1-install.bat 重新打包。环境变量配置我一般这样配JAVA_HOME指向 JDK 安装根目录PATH里加%JAVA_HOME%\bin配完重开 cmd 才生效。5.2 页面能打开但登录后请求全返回 404 或 405现象浏览器能正常显示登录页输入账号密码点击登录控制台或页面提示404 Not Found或405 Method Not Allowed。原因前端调用的接口路径和后端 Controller 的RequestMapping不一致端口不一致更常见。前端 axios baseURL 还指向作者本机比如http://localhost:8080/api你改了后端端口但前端没改。解决打开浏览器 F12 Network 面板看请求地址是什么。通常前端代码里会有一个request.js或http.js里面定义了axios.defaults.baseURL直接改成你的后端地址。如果地址对但 404就去后端 Controller 比对RequestMapping的路径前后端接口路径是全拼的还是下划线风格一不一致。5.3 启动时提示数据库连接失败现象启动日志里出现Cannot create PoolableConnectionFactory或者Access denied for user rootlocalhost。原因application.yml里的数据库名、用户名、密码不对或者 MySQL 服务没开或者数据库还没导入 SQL 脚本。解决先看url里的库名对应不存在。用 Navicat 或命令行连接 MySQL执行show databases;检查有没有那个库。没有就建库再导入 SQL。确认账号密码能正常登录注意 MySQL 8 的加密规则和连接驱动。附带说一下如果你连接成功但提示Unknown database xxx那说明脚本还没执行去项目 doc 目录找hospital.sql之类的文件导入。5.4 库存被扣成负数然后业务数据全乱现象反复测试出库功能后数据库里这条药品的库存数量是负数。原因出库代码是“先查库存再拼接 update”没有在 update 里加quantity #{quantity}条件也没有用事务把更新和插入流水包起来。并发请求下必然出负数单请求下如果接口被人绕过页面直接调传一个超大数量也会扣穿。解决把你手上的出库 SQL 改成第 4 章那种条件更新WHERE drug_id ? AND quantity ?。Service 方法加Transactional。如果这套代码里已经写了那要检查是不是 Service 层没加事务或者扣减时传入的数量已经被前端动了手脚。血泪经验库存相关代码不要相信前端传过来的任何数字必要时在后面校验一下业务语义。5.5 静态资源拦截器导致页面白屏现象能访问http://localhost:8080/index.html但页面上 js 和 css 全部加载失败控制台报了一堆 403 或者 401。原因你在WebMvcConfig里配置拦截器时用了addPathPatterns(/**)把所有路径都拦了静态资源请求也被preHandle拦住返回 401。静态资源的 handler 不是HandlerMethod但你写的拦截器没判断类型直接当接口处理了。解决给拦截器用的addPathPatterns缩到/api/**只拦接口不拦静态资源同时排除登录、注册、验证码等公开接口。参考第 3 章里WebMvcConfig的写法这个姿势基本能避免 99% 的白屏问题。如果你已经用/**并且想在拦截器里兜底那就必须在preHandle开头加一句if (!(handler instanceof HandlerMethod)) { return true; }6. 给药品有效期加自动预警Scheduled 定时任务实战这套系统的药品表里有“有效期”字段但大多数毕设版本只存不报警药品过期了系统毫无反应。答辩时如果老师问“药房最怕什么”答案之一就是药品过期。我拿到这套系统后做的第一个增强就是把有效期预警补上代码改动不大效果却很直观。先启动类加定时任务开关SpringBootApplication EnableScheduling public class HospitalApplication { public static void main(String[] args) { SpringApplication.run(HospitalApplication.class, args); } }然后写一个检查任务Component public class DrugExpireTask { private static final Logger log LoggerFactory.getLogger(DrugExpireTask.class); Autowired private DrugMapper drugMapper; Scheduled(cron 0 0 2 * * ?) public void checkExpireDrug() { // 查出30天内到期的药品 ListDrug expireSoonList drugMapper.selectNearExpire(30); if (expireSoonList.isEmpty()) { return; } for (Drug drug : expireSoonList) { drug.setStatus(2); // 2临期 drugMapper.updateById(drug); } log.warn(共 {} 种药品即将到期已更新状态, expireSoonList.size()); } }Mapper 里的 SQLselect idselectNearExpire resultTypecom.hospital.entity.Drug SELECT * FROM drug WHERE valid_date IS NOT NULL AND valid_date BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL #{days} DAY) /select逻辑说明Scheduled(cron 0 0 2 * * ?)前五位分别表示秒、分、时、日、月?表示星期不指定这里是每天凌晨 2 点执行。选择凌晨 2 点是因为这个时间点系统访问量低跑批任务不会影响白天业务。DATE_ADD(NOW(), INTERVAL #{days} DAY)计算 30 天后的时间窗口凡是有效期落在这个区间内的药品都视为临期。跑完任务前端药品列表刷新之后临期药品自动带上状态标记。如果再配合库存报警首页就同时能看到“3 种药品库存不足5 种药品即将到期”这个细节直接写进论文的“系统创新点”部分比那些通篇只说 CRUD 的强太多。从那以后我接手的每一个毕设项目第一件事都是先看有没有这种“该主动推送却只能被动查询”的业务点比如库存、有效期、生日提醒、定时备份全部统一用Scheduled补上然后再去动主体代码。这套医院药品管理系统的改造思路也是按这个顺序来的。希望帮到你。本文还有配套的精品资源点击获取