ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于 Spring Boot 的智能药箱系统:Java 毕设全栈实战与部署指南

基于 Spring Boot 的智能药箱系统:Java 毕设全栈实战与部署指南 做了几个 Spring Boot 方向的 Java 毕设项目之后我最推荐拿来练手的就是“智能药箱系统”。这个题目是典型的 JavaWeb 全栈实战后端用 Spring Boot前端配 Vue 或者 Thymeleaf数据库走 MySQL核心业务围绕“服药时间提醒”展开。功能边界清晰难度适中数据模型也好设计关键是答辩的时候特别好讲——既有业务场景又有可演示的代码逻辑还不至于复杂到要把人绕晕。项目打包好了完整源码、LW论文文档、部署说明和演示视频等于一份毕设该有的交付物一次性配齐。这篇就结合我做下来的完整过程把设计思路、核心模块实现、部署步骤和踩过的坑全部整理出来给正在做 Java 毕设的同学一个可以直接照着落地的参考。先说说这个系统到底在解决什么问题。家里有老人需要长期服药的最头疼的就是漏服、错服、重复服药子女上班没法时刻盯着。药箱系统把“药”和“提醒”串起来家属帮老人建档、维护药品信息、设置每天哪几个时间点吃什么药到点了系统推送提醒老人可以手动确认“已服药”家属端能看到记录。听起来简单但落到代码上要处理用户权限、用药计划、定时扫描、消息推送、记录统计这一整套逻辑。这恰恰就是毕业设计需要的体量——工作量足够技术点覆盖全面又不会失控。1. 项目整体设计与选型思路1.1 为什么选“智能药箱”作为毕设题目选毕设题目有个基本原则业务场景自己要有感知技术栈要能展示水平工作量要可控。智能药箱的优势就在于它是个“看得见摸得着”的物联网式业务系统不像纯电商那样一堆订单购物车模板感太重也不像纯后台管理系统那样干巴巴的没有亮点。服药提醒这件事存在明显的“家庭协作”属性所以角色划分天然就是多角色的患者被提醒的人、家属维护计划和查看记录的人、管理员维护药品字典和系统数据。多角色就意味着需要权限控制这是答辩评委一定会关注的点。另外系统里还涉及库存预警、药品有效期提醒、服药统计报表这些业务功能随便一个都能撑起一个模块的论述整体做完功能清单拿出去是完全够看的。从技术面的角度这个项目涉及后端 CRUD、定时任务、消息推送、文件上传、数据统计每个点都是 Java 面试里常被追问的内容做完之后对面试也有直接帮助。1.2 Spring Boot 技术栈选型与理由后端框架直接用 Spring Boot。我这里用的是Spring Boot 2.7.x搭配JDK 8没有上 3.x。为什么这么选Spring Boot 3.x 强制要求 JDK 17API 上javax包全换成了jakarta很多老教程和老依赖会出现兼容问题。毕设最怕的不是功能多而是环境配不通、代码跑不起来。2.7.x 是 2.x 系列的最后一个稳定大版本技术成熟、资料丰富、兼容性最好等系统跑通了你自然明白版本升级是怎么回事。其他组件选型组件选型说明持久层MyBatis-Plus内置单表 CRUD省掉大量重复的 XML 编写数据库MySQL 5.7/8.0开源、普及率高答辩环境一般都有权限Spring Security JWT接口安全、角色控制技术亮点前端Vue 3 Element Plus前后端分离开发体验好定时任务Spring 原生 Scheduled单体系统不需要引入重型的 Quartz构建Maven毕设标配别用 Gradle 增加没必要的复杂度辅助Lombok、Hutool省样板代码集成一些工具方法用 MyBatis-Plus 做持久层真的省事。单表的增删改查几乎不用写 SQL调用IService自带的方法就行。比如用药计划表的新增直接在 Service 里save(plan)就能落库。省出来的时间全花在业务逻辑上性价比非常高。1.3 系统整体架构与角色权限设计系统定位是“前后端分离的单体应用”前端部署在 Nginx后端是一个 Spring Boot 的 jar 包数据库独立部署。项目的目录结构按模块分包src/main/java/com/example/medbox ├── config // 全局配置、安全配置、定时任务配置 ├── controller // 接口层 ├── service // 业务层 ├── mapper // 数据访问层 ├── entity // 实体类 ├── dto // 前端交互数据对象 ├── common // 统一返回结果、异常处理 └── task // 定时提醒任务角色权限核心设计成三个角色核心能力权限特点患者Patient查看自己的提醒消息、确认服药、查看用药计划数据范围仅限本人家属Family维护患者档案、维护药品和用药计划、查看服药记录和提醒报告可操作多个已绑定患者管理员Admin药品字典维护、用户管理、系统参数配置后端管理功能全开权限这块用 Spring Security JWT 实现。登录成功后签发 Token前端请求头里带Authorization后端通过拦截器把当前用户信息放入上下文中。接口上通过PreAuthorize(hasRole(FAMILY))这种注解控制访问权限。这里一个小建议角色权限不要做太复杂能讲清楚 JWT 的校验流程和拦截原理就够了过度的权限模型会让代码变得臃肿答辩也不好讲。2. 核心功能拆解与数据库设计2.1 功能模块拆解整个系统从使用流程上分可以拆成六大功能模块用户管理与登录注册注册以后默认是“家属”角色支持绑定患者档案一个家属可以绑定多位老人比如自己的父母。药品档案管理录入药品名称、规格、生产日期、有效期、用法、库存数量。管理员维护的“药品字典”提供下拉选择家属录入时不用重复输入。用药计划管理这是核心业务。家属为患者选择药品设置服用剂量、服药方式、每日服药次数和对应的时间点。支持“每天固定时间点”如 08:00、20:00和“每隔 N 小时”两种模式。定时提醒任务后台每分钟扫描一次当前时间需要触发的用药计划生成提醒消息并通过站内消息、邮件或 WebSocket 推送给患者和家属。服药记录与统计患者点击“确认已服药”后生成记录。没确认的可以标记“漏服”。家属端可以按日期/药品维度查看依从率统计。库存和有效期预警药品库存低于阈值时生成预警距离过期失效不足 30 天的在首页红字提醒。模块之间是有依赖链条的药品档案是基础用药计划依赖药品定时任务依赖计划记录又依赖任务推动。这种“链式依赖”本身就是很好的答辩素材可以画数据流转图也可以讲清业务闭环。2.2 数据库核心表设计数据库设计是答辩一定会被问的环节。智能药箱系统我设计了 8 张核心表这里展示最关键的几张。第一张是用户表sys_userCREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码(BCrypt), real_name varchar(50) DEFAULT COMMENT 真实姓名, phone varchar(20) DEFAULT COMMENT 手机号, role varchar(20) NOT NULL DEFAULT FAMILY COMMENT 角色, status tinyint DEFAULT 1 COMMENT 状态: 1正常 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;第二张是药品表med_medicine字段包含通用信息。注意expire_date一定要存DATE类型这样日期比较直接用 SQL 就能算不用在 Java 里做字符串比较。第三张是用药计划表med_plan这是整个系统的核心表CREATE TABLE med_plan ( id bigint NOT NULL AUTO_INCREMENT, patient_id bigint NOT NULL COMMENT 患者ID, medicine_id bigint NOT NULL COMMENT 药品ID, dose varchar(50) DEFAULT COMMENT 每次剂量如: 1片/5ml, plan_type tinyint NOT NULL DEFAULT 1 COMMENT 1-固定时间点 2-间隔小时, remind_time varchar(20) DEFAULT COMMENT 固定时间点格式HH:mm, interval_hours int DEFAULT NULL COMMENT 间隔小时数, start_time datetime NOT NULL COMMENT 开始日期, end_time datetime DEFAULT NULL COMMENT 结束日期, status tinyint DEFAULT 1 COMMENT 1启用 0停用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用药计划表;remind_time我用的是varchar直接存字符串“08:00”这种格式。为什么不拆成小时和分钟两个字段因为解析LocalTime太方便了一行代码的事。而“间隔小时”模式在定时扫描的时候单独处理不依赖remind_time字段。第四张是服药记录表med_take_recordCREATE TABLE med_take_record ( id bigint NOT NULL AUTO_INCREMENT, plan_id bigint NOT NULL COMMENT 计划ID, patient_id bigint NOT NULL, medicine_id bigint NOT NULL, remind_time datetime NOT NULL COMMENT 计划提醒时间, take_time datetime DEFAULT NULL COMMENT 实际确认服药时间, status tinyint DEFAULT 0 COMMENT 0-未处理 1-已服药 2-已漏服, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT服药记录表;这张表是把“定时扫描”和“用户确认”连接起来的枢纽。定时任务扫描到某个计划该提醒了先插入一条status0的记录同时在提醒消息表里生成一条消息。等用户点击“已服药”把这条记录的status改成1。如果一直没点那么到晚上 22:00 或者第二天凌晨统一跑一个补偿任务把当天还未处理的记录标记为2漏服。2.3 服药提醒时间的规则设计细节提醒规则是整个项目最核心的业务逻辑也是答辩最容易展开讲的地方。我把它拆成两个模式固定时间点模式用户选择了“一天两次”分别在 08:00 和 20:00。存储层面就是把plan_type设为 1然后在remind_time字段用逗号拼接存储比如08:00,20:00。虽然不够范式化但对于毕设来说最直观查询时解析一下字符串就行。定时任务每分钟扫描一次String sql SELECT * FROM med_plan WHERE plan_type 1 AND status 1 AND .concat(parseRemindTimeCondition());实际用 MyBatis-Plus 写的话就是查出所有启用的固定时间点计划在内存里面做LocalTime.now()和计划时间比对。因为数据量很小这种内存比对方式完全没问题而且逻辑透明好讲。间隔小时模式比如“每隔 6 小时一次”那么今天 08:00 第一次服药下次就是 14:00、20:00……这个模式就不能单靠计划表存时间实现了需要结合“最后服药时间”last_take_time。实现思路是每次用户确认服药后更新该计划的last_take_time为当前时间定时扫描时检查now - last_take_time interval_hours如果满足就生成新的提醒。这样设置有个好处用户连续漏服两次后系统依然能基于最后一次服药时间继续推算出下一轮提醒不会出现时间堆积错乱。这里有一个我实际开发中踩过的坑如果“固定时间点”和“间隔小时”共用一张计划表查询时就要特别注意plan_type的过滤条件。最开始我把两类的判断条件混在一起写结果“固定时间点”的计划也会被“间隔小时”分支扫到导致重复提醒。后面把查询条件抽成了两个独立的方法一个查plan_type 1一个查plan_type 2互不影响问题彻底解决。3. 关键模块实操实现与项目部署3.1 基于 Spring 原生 Scheduled 的定时提醒任务定时任务是整个系统的“发动机”。实现方式就是在启动类上加上EnableScheduling然后在任务类方法上标Scheduled(cron 0 0/1 * * * ?)让方法每分钟执行一次。Component Slf4j public class RemindTask { Autowired private MedPlanService medPlanService; Autowired private TakeRecordService takeRecordService; Scheduled(cron 0 0/1 * * * ?) public void scanRemind() { ListMedPlan plans medPlanService.listEnabledPlans(); LocalTime now LocalTime.now(); for (MedPlan plan : plans) { if (plan.getPlanType() 1) { handleFixedTimePlan(plan, now); } else if (plan.getPlanType() 2) { handleIntervalPlan(plan); } } } private void handleFixedTimePlan(MedPlan plan, LocalTime now) { String remindTimes plan.getRemindTime(); // 08:00,20:00 String[] times remindTimes.split(,); for (String time : times) { LocalTime target LocalTime.parse(time.trim()); if (now.getHour() target.getHour() now.getMinute() target.getMinute()) { generateRemind(plan, LocalDateTime.now()); } } } }扫描逻辑简单来说就是“每个整分钟把当前时间与计划时间比对相等就触发”。有一个细节必须注意方法的执行时间如果恰好跨分钟比如 08:00:59 才执行到这条计划比对getMinute()就出问题了。我现在的写法是把分钟号存到一个Set里做去重同一个计划在一分钟内最多只触发一次避免重复推送。实现方式是加一个内存标记比如在计划实体上维护一个lastRemindDate字段每次生成提醒前判断是否已经有当天的记录。3.2 提醒消息多渠道触达设计提醒生成之后怎么触达到用户我做了三层设计站内消息写入med_message表前端首页右上角红点提示点击进去能看到提醒详情。这是毕设最稳妥的展示方式也方便录演示视频。WebSocket 实时推送患者和家属端保持 WebSocket 连接服务端一旦生成新提醒立刻推送到前端弹窗同时播放提示音。自己写 WebSocket 其实不复杂用 Spring 的TextWebSocketHandler 拦截器鉴权就行。第三方通知预留接口短信、微信通知属于成熟服务个人项目申请不到服务商资质所以这块我用接口预留的方式处理定义好NotifySender接口先写一个LogNotifySender把消息打到控制台模拟发送。这样论文里可以写“支持接入阿里云短信/微信公众号模板消息”答辩时也能自圆其说。这里要特别强调一下设计上的取舍不要为了“看起来高级”硬上消息中间件。RabbitMQ、Kafka 这些组件在单体毕设里根本没有场景引入反而会让自己陷入环境搭建的泥潭。一个接口 两个实现类既展示了面向接口编程的思维又不会影响系统稳定性。3.3 前后端联调与 IDEA 配置要点后端接口开发完成后用 IDEA 启动 Spring Boot默认端口 8080。有几点经验参考IDEA 配置启动端口默认情况下 Spring Boot 的启动端口从application.yml读取server: port: 8080 servlet: context-path: /api我习惯加一个context-path: /api这样后端接口统一带/api前缀前端代理时就不容易和后端静态资源路径冲突。如果想在 IDEA 里临时设置端口可以在Run/Debug Configurations的VM options里加-Dserver.port8081覆盖 yml 配置。这个技巧在端口被占用时特别好用。前端 Vue 联调代理开发环境下在 Vue 项目的vite.config.js里配置代理避免跨域server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }生产环境把 Vue 打包进 Spring Boot这个是很多同学卡住的点。毕设为了部署方便往往不单独部署前端服务而是把 Vue 打包后的dist/目录塞进 Spring Boot 的src/main/resources/static/这样直接启动 jar 包就能访问页面。注意几个地方打包前要把接口请求地址改为相对路径Vue Router 要用createWebHashHistory()不要用createWebHistory()否则刷新页面会 404打包后的index.html里引用的 JS/CSS 路径必须是./开头。这几个坑我都踩过前两个尤其隐蔽。3.4 项目打包与 Linux 服务器部署全流程毕设评审通常要求系统能现场跑起来。我用的部署方式是打包成 jar 包 Nginx 反向代理 宝塔面板管理 MySQL。后端打包mvn clean package -DskipTests打包出来的 jar 在target/目录下把这个文件传到服务器用命令启动java -jar medbox-1.0.0.jar --spring.profiles.activeprod生产环境配置文件挂在同目录下的application-prod.yml并设置数据库连接。需要注意 MySQL 必须开启远程连接或者在部署机本地装 MySQL 并用上数据库账号密码。Nginx 配置如下server { listen 80; server_name your-domain.com; location / { root /home/www/medbox-front; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }前端页面走 Nginx 的静态文件服务后端接口走反向代理。一个值得注意的坑proxy_pass的 URI 要写成http://127.0.0.1:8080/api/而不是http://127.0.0.1:8080/否则后端接口路由就丢了/api前缀。部署完成后的自测路径也很固定先访问前端页面确认能打开 → 再看登录接口能否通 → 再测核心的创建用药计划和提醒消息。用 Chrome 的 Network 面板能看到请求是否 200、响应结构是否正常这一步能省下大量排查时间。4. 毕设常见问题与答辩避坑实录4.1 Spring Boot 版本与 JDK 兼容性大坑这个可能是做毕设时遇到最多的问题尤其最近几年新教程全在推 Spring Boot 3.x但很多同学本机还装着 JDK 8。Spring Boot 3.x 必须 JDK 17。如果你下载的源码是 3.x 版本本机却是 JDK 8那启动必然报UnsupportedClassVersionError。还有更隐蔽的Spring Boot 3.x 把javax.servlet全部改成了jakarta.servlet。如果你的代码里还有import javax.servlet.http.HttpServletRequest;在 3.x 环境下一定会编译失败。但这个问题在 2.x 环境是正常的。所以拿到项目源码第一件事就是检查pom.xml里的 Spring Boot 版本和本机 JDK 版本对照表整理如下Spring Boot 版本JDK 要求javax/jakartaMyBatis-Plus 适配版本2.7.xJDK 8 或 11javax3.5.x3.2.xJDK 17 或 21jakarta3.5.3.2 或使用 mybatis-plus-spring-boot3-starter如果只知道“用最新版”很容易就把自己坑进去。我的建议很直接毕设环境统一用 Spring Boot 2.7.x JDK 8绝对稳定。这个组合能覆盖绝大多数教程和源码案例。4.2 定时任务阻塞与重复提醒问题Scheduled默认情况下是单线程串行执行的。什么意思如果你在系统里写了两个定时任务任务 A 执行耗时 5 分钟那么任务 B 就算到了触发时间也必须等任务 A 跑完才能执行。我的系统刚开始没注意这个问题导出的统计报表任务和提醒任务互相卡提醒全乱套了。解决办法是在配置类里自定义一个线程池来隔离任务Configuration EnableScheduling public class SchedulingConfig implements SchedulingConfigurer { Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler new ThreadPoolTaskScheduler(); scheduler.setPoolSize(5); scheduler.setThreadNamePrefix(medbox-task-); return scheduler; } }线程池设成 5提醒、统计、库存预警各跑各的互不干扰。这个配置建议所有做定时任务的同学直接抄下来不会出错。另一个问题就是重复提醒。如果不做任何清洗假设服务器性能有问题某一分钟内的任务跑了两次那患者就会收到两条完全一样的提醒消息。我的方案是在生成提醒前查一下med_message表里是否已经存在“该计划 该提醒日期 未读取”的记录存在就跳过。这相当于在消息生成层面做了一次幂等。4.3 数据库乱码与时间差问题中文乱码是老生常谈但每次都能坑到人。MySQL 建表字符集必须统一用utf8mb4如果你的服务器 MySQL 默认字符集是latin1连接字符串要加上参数jdbc:mysql://localhost:3306/medbox?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai时间问题更值得注意。服药提醒是一个对时间极度敏感的系统如果服务器时区是 UTC而你的业务用北京时间那么定时扫描出来的时间会差 8 个小时早上 8 点变成中午 16 点。所以在application.yml中一定要设置spring: jackson: time-zone: Asia/Shanghai date-format: yyyy-MM-dd HH:mm:ss同时在 MySQL 连接 URL 里加serverTimezoneAsia/Shanghai。这两处都设置好时间戳才不会有偏差。4.4 论文 LW 撰写与答辩高频问题论文这部分很多同学是“项目做完了文档不知道怎么写”。智能药箱这个项目的 LW 章节结构我的经验是这么安排绪论背景意义、国内外现状、相关技术介绍Spring Boot、MyBatis-Plus、Vue、JWT、系统需求分析功能性需求、非功能性需求、用例图、系统设计架构设计、数据库设计、接口设计、系统实现各模块截图 核心代码讲解、系统测试功能测试表、性能测试数据、总结与展望。这七章足够撑起一篇条理清晰的毕设论文了。写论文一定要记着每个功能模块要有截图 代码片段 文字说明三者缺一不可。评审老师不可能一行行读代码但他们会看你怎么描述自己的代码。描述时多写“通过 XX 实现了 XX这样设计的原因是 XX”这种句式最贴合学术写作习惯。答辩的提问方向我根据自己的经验列一下高频题目高频问题回答要点为什么用 Spring Boot 不用 SSM自动配置、内置容器、starter 生态、开发效率定时任务怎么实现会不会阻塞Scheduled 扫描 线程池对比 Quartz 差异如果提醒消息发送失败怎么办消息表留状态位失败重试预留第三方接口系统最多支持多少用户单体架构下 500 并发以内性能优化可以上 Redis 缓存数据库为什么这么设计从业务模型出发解释表关系、主外键逻辑4.5 拿到“完整源码一条龙”之后你真正要做的事现在网上很多标题挂着“完整源码LW部署说明演示视频”我理解大家的心理想省事。但能做完整套项目交付不等于你能答辩通过。光把项目跑起来只是第一步要真正吃透它我建议你在拿到项目后按这个顺序走一遍先跑通按部署说明把项目在本地 IDEA 里启动起来数据库导入 SQL接口全部测一遍。画架构图自己不看源码把系统的功能模块图、数据库 ER 图画出来。画不出来的地方就是你还没搞懂的地方。改一个需求找一个不起眼的小功能自己动手改一遍比如把“固定时间点”改成支持“周一到周五执行”。这个“微改造”会在答辩时成为你的护身符——老师一听你能现场改代码基本不会再追问过深。提前模拟答辩把上面那张问题表打印出来找同学模拟提问提前组织回答口径。演示视频的录制也有讲究。我录的时候发现直接把屏幕一录容易乱后来统一了口径先展示登录 → 添加药品 → 给患者创建用药计划 → 设置一个马上到点的提醒 → 快速演示推送和确认服药 → 打开统计页看报表。一条业务主线走完整个过程不超过 5 分钟但系统的每个核心功能都覆盖到了。演示前一定要检查系统时间和你设置的提醒时间差不然等提醒等了两分钟没反应现场会很尴尬。我个人在实际操作中最大的体会是智能药箱这个项目难点不在功能多而在于“提醒”这个业务对准确性要求极高在任务调度、时间比对、幂等去重这些细节上多花心思项目的质感会明显上一个档次。最后再分享一个小技巧——给用药计划表加一个test_mode测试开关字段演示时把提醒间隔临时调成 30 秒录出来的视频节奏紧凑看起来效果专业很多。这个字段虽然不复杂但在答辩现场是真能救场的。
返回列表