ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

SpringBoot+Vue医院药品管理系统:库存闭环、权限认证与定时预警实战

SpringBoot+Vue医院药品管理系统:库存闭环、权限认证与定时预警实战

简介:这是一套面向高校毕业设计及课程设计的医院药品管理系统完整项目,基于SpringBoot开发后端服务,搭配Vue构建前端界面。功能模块覆盖用户注册登录、角色权限控制、药品信息与分类管理、实时库存监控、采购申请与审批流程,贴合医院药品管理常见业务场景,可作为计算机专业学生毕业设计或期末项目的参考模板。资源压缩包共415个文件,整体约24.08MB,主要包含Java源码、Vue组件、SVG图标、XML配置等开发文件,同时提供SQL数据库脚本、bat启动脚本和演示视频,方便直接导入数据库、运行前后端并查看操作流程。已有59人学习。借助这份资料,既能学习SpringBoot+Vue的模块化编码与前后端交互,也能获得论文撰写时的功能划分思路、数据库设计方法和项目演示素材,是比较完整的毕设解决方案。

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_HOME=D:\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_HOME=D:\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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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 脚本先创建库,不然启动时数据源初始化直接报错。serverTimezone=Asia/Shanghai解决 MySQL 8 的时区问题,如果你用的是 MySQL 5.7,serverTimezone可以保留也可以去掉。driver-class-name这里写的是com.mysql.cj.jdbc.Driver,MySQL 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.port=8081

改成 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

密码这块,毕设项目里 90% 都是 MD5 处理,DigestUtils.md5DigestAsHex(password.getBytes())即可。但你如果用 BCrypt,答辩证时反而能多说一层“MD5 可被彩虹表破解,BCrypt 自动加盐”。后端登录接口通常长这样:

@RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @PostMapping("/login") public Result<User> 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 Result<List<Drug>> 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 Set<String> getUserPermissions(Long userId) { return userMapper.selectPermissionsByUserId(userId); }

然后拦截器校验逻辑变成:

Set<String> permissions = (Set<String>) 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 表信息量大得多。

出库扣减是库存系统最核心的代码,直接决定会不会出现负数。我一般这样写 Service:

@Service 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 id="reduceStock"> 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),不够就抛异常不行吗?行,但高并发下不行。两个请求同时读到 quantity=15,一个出库 12 一个出库 5,各自校验都通过,最终库存变成 -2。而 SQL 条件更新在数据库层面是原子的,同一个 drug_id 的更新会自动排队,第二个请求执行时 quantity 已经变成 3,quantity >= 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 id="selectDrugWithStock" resultType="com.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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

药剂师提交采购申请后,管理员审核接口改变状态:

@PostMapping("/purchase/audit") @RoleRequired({"admin"}) public Result<Void> 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 盘,脚本就崩了。
  • 解决:不要直接双击。先打开 cmd,cd 到项目目录,手动执行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 'root'@'localhost'。
  • 原因: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天内到期的药品 List<Drug> expireSoonList = drugMapper.selectNearExpire(30); if (expireSoonList.isEmpty()) { return; } for (Drug drug : expireSoonList) { drug.setStatus(2); // 2=临期 drugMapper.updateById(drug); } log.warn("共 {} 种药品即将到期,已更新状态", expireSoonList.size()); } }

Mapper 里的 SQL:

<select id="selectNearExpire" resultType="com.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补上,然后再去动主体代码。这套医院药品管理系统的改造思路也是按这个顺序来的。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表