每年三四月份,总有不少人在群里问同一个问题:Java毕设到底做什么题目,才能既稳过、又有东西能讲。如果你正在纠结,我建议你认真看看“基于Spring Boot的智能药箱系统”这个方向。它不飘、不虚,一个人完全能驾驭,技术上覆盖了Spring Boot后端、MySQL数据库设计、权限控制、定时任务、消息通知、数据可视化,几乎把Java后端日常开发该碰的东西都碰了一遍,而业务背景又贴近家庭用药这个真实场景,中期检查和答辩的时候都有故事可讲。
做这个题目比较舒服的一点是:功能边界清晰,不会像电商系统那样越做越大收不住,也不会像纯粹的管理系统那样做完自己都没兴趣讲。智能药箱系统天然带两个关键词:一个是“管理”,一个是“智能”。管理对应药品档案、库存、家庭成员、用药记录;智能对应定时提醒、过期预警、用药统计。这两块组合起来,既能体现工程能力,又能体现一点产品思维。
适合谁呢?两类人。一类是Java方向毕业设计选题的同学,需要一套架构清晰、可解释、能落地的系统;另一类是刚学完Spring Boot想找个完整项目练手、把CRUD之外的技术点串起来的开发者。这篇文章我就按照一个实际项目的思路,从设计到实现、再到调试和定制,把这些东西完整展开。
1. 项目定位:为什么智能药箱是毕设的“高性价比”选择
1.1 题目到底在考察什么
毕设评审老师看一个项目,通常不只看功能多不多,而是看三个东西:有没有明确的问题背景,技术选型是否合理,核心功能是否能闭环。
智能药箱这个题目,问题背景天然成立。老年人忘记吃药、重复吃药、药品过期还在用,这是真实世界长期存在的痛点。系统目标也很清楚:把“家里一堆药随便吃”变成“有计划的、有记录的、有提醒的”规范化服药管理。有了这个背景,开题报告和论文的“研究意义”部分就不会写成空洞的套话。
技术选型方面,Spring Boot是目前Java后端最主流的快速开发框架,内置了Tomcat、自动配置、起步依赖,对毕设来说,用它能省掉大量繁琐的XML配置,把精力放到业务上。评审老师大概率也认可这个选型,因为这确实是生产环境里常用的技术栈。
功能闭环的意思是:用户登录→添加药品→设置服药计划→系统定时提醒→用户确认服药→系统记录并统计。这条链路从头到尾是通的,不是只做了几个CRUD页面就完了。这一点在答辩现场尤其重要,老师顺着一条业务线问下来,你能把每个环节的实现都说清楚,这个项目基本就稳了。
1.2 为什么Spring Boot是最稳的选择
我见过有同学选SSH(Struts2 + Spring + Hibernate)或者纯Servlet做类似题目,不能说不行,但确实给自己挖坑。老框架资料少、配置麻烦,遇到问题查起来费劲;纯Servlet则几乎所有东西都要手写,编码、分页、事务、JSON解析,每一项都在消耗答辩前宝贵的时间。
Spring Boot的好处在于:
- 起步依赖帮你把版本兼容问题解决了,Spring Boot 2.x 配 MyBatis 或 JPA 都是现成starter,几行配置就能跑起来。
- 内置Tomcat,打成一个jar包就能运行,部署演示非常方便。
- 全家桶组件丰富,做权限有Spring Security,做定时任务有@Scheduled,做接口校验有Validation,以后想扩展消息队列、缓存也都有现成方案。
还有一个隐形好处:Spring Boot项目结构是事实标准,网上资料多,遇到bug搜索关键词一搜一大把,这对毕设阶段来说极其重要。你用Spring Boot,等于站在绝大多数Java开发者踩过的坑上面,而不是独自在边缘框架里探险。
2. 智能药箱系统的功能模块怎么拆
2.1 药品档案与库存管理
药品管理是整个系统的数据基础。简单说,就是给每一盒药建一个“身份档案”,后续的过期提醒、库存预警、服药记录全部依赖这份档案。
操作上,药品档案一般包括:药品名称、通用名、规格、生产厂家、生产日期、有效期、库存数量、库存预警值、用药说明、药品图片。
这里有几个细节值得注意。第一,有效期字段建议精确到“日”,不要只存年份,因为药品过期提醒判断的是“距离过期还剩多少天”。第二,库存不要一开始就做成复杂的进销存,毕设阶段做到“入库+扣减+预警”就够了:药品被添加时录入初始库存,服药确认后按单次用量扣减,当库存低于预警值时在首页给出提醒。第三,药品图片推荐存URL路径而不是Base64大字段,否则页面加载会明显变慢,数据库也会膨胀。
我还建议加一个“过期药品”模块,用定时任务每天扫一次所有药品的有效期。逻辑上分三档:已过期、30天内临期、正常。这样首页能展示一个直观的“家庭用药健康度”统计。
2.2 服药计划与定时提醒
这是整个系统的灵魂功能,也是答辩时最能讲出东西的部分。
服药计划本质是一张业务表,记录“谁(家庭成员)、什么时候(时间点)、吃什么药(关联药品)、吃多少(剂量)、持续多少天(周期)”。
提醒功能不要只做成一个固定的“每天8点提醒我吃药”。更合理的做法是支持不同频率:每天一次、每天多次、隔天一次、每周几次、在某个时间段内连续服用。每种频率对应的就是一个定时规则,用Cron表达式来描述最方便。
比如每天早晨8点吃一次降压药,Cron就是 0 0 8 * * ?;隔天一次,可以写成 0 0 8 */2 * ?;每周一和周四晚上9点,则是 0 0 21 ? * MON,THU。Cron表达式这块是评审老师比较喜欢问的,把几个典型规则背熟,能现场解释明白,会给你加分不少。
提醒执行时,系统要做三件具体的事:判断当前时间点是否有待执行的服药计划;如果有,就按该计划关联的通知渠道推送提醒;推送之后等待用户反馈,是“已服药”还是“跳过”。回执数据写进服药记录表,后续统计就靠它。
2.3 家庭成员与角色权限怎么设计
智能药箱面向的不是单个用户,而是一个家庭。家里可能有老人、子女、照护人员,不同身份能做的事不一样。
这里建议用经典的RBAC模型,用户-角色-权限三层。角色先简单设计三种:管理员(一般是子女或家庭管理者),成员(普通家庭成员),访客(比如护工、亲戚)。权限上,管理员可以维护药品档案、管理家庭成员、查看所有服药记录;成员可以给自己添加服药计划、确认服药;访客只能查看部分数据。
技术实现上,如果不想把毕设做太重,不必上Spring Security的完整过滤链也能做好,用一个HandlerInterceptor做登录拦截,配合JWT或者服务端Session做身份识别,再写一个自定义权限注解来控制接口访问,完全够用。但如果你想让项目看起来更有分量,用Spring Security + JWT也是正规路子,就是配置复杂一点,到时候得多花时间调试。
这里提醒一句:权限不是功能越多越好,而是要能回答“某个角色通过什么方式访问了哪些接口”。答辩老师可能会问“如果我以普通成员身份调用管理员接口会怎样”,你在代码里把拦截规则指出来,现场演示一下401/403返回,这个问题就答得很漂亮。
2.4 药箱设备与用药记录怎么衔接
作为“药箱”系统,总不能完全跟硬件没关系。毕设阶段最常见的做法是硬件模块做“轻量模拟”或“预留接口”。
一种做法是定义一个DeviceController接口,后端只负责接收设备上报的状态,比如开关状态、温湿度数据、取药仓门是否打开。这些数据可以手动模拟,也可以在项目里写一个定时生成模拟数据的工具类,每隔一段时间随机生成一条设备数据写入库中。这样系统页面上的“设备状态”看起来就是活的,答辩演示效果会好很多。
另一种做法是预留真实硬件接入。如果你手头有树莓派、传感器模块或者至少一个能写数据的客户端程序,可以通过HTTP接口或者MQTT协议把真实数据推送进来。Spring Boot这边做一个简单的MQTT客户端或者HTTP端点接收数据。这个属于加分项,不是必须,但如果你论文里写了“系统预留物联网接口”,那最好真有一个能跑的模拟客户端,避免答辩时只拿嘴讲。
用药记录是整个系统的“结果数据”。每一次确认服药,都会生成一条记录:谁、什么药、什么时间、剂量多少、是按时服用还是补服。这个表数据量会慢慢涨起来,也为后续统计图表提供了原料。
3. 实操过程:数据库设计与关键实现
3.1 数据表该怎么设计
我直接把核心表结构思路说一下,照着这个思路建表不会偏。
用户体系三张表:sys_user、sys_role、sys_user_role。如果简化,也可以把角色直接做成用户表的一个字段,但那样扩展性差一点。毕设为了体现设计能力,建议保留三张表。
药品与库存:medicine,字段包括medicine_id、name、generic_name、spec、manufacturer、production_date、expiry_date、stock、warning_threshold、img_url、remark、create_time。
服药计划:medication_plan,字段包括plan_id、user_id、medicine_id、dose、frequency(存放Cron表达式或自定义枚举)、start_date、end_date、status(启用/停用)。
服药记录:medication_record,字段包括record_id、plan_id、user_id、medicine_id、take_time、status(已服用/已跳过/补服)、remark。这个表的查询频率高,建议在user_id和take_time上建索引。
设备与通知:device_info、notify_log。device_info记录药箱设备ID、状态、上报时间;notify_log记录每一次提醒推送的渠道、内容、结果,方便排查“为什么没收到提醒”。
表名要注意一个坑:user、order、system这类词在部分数据库版本里是保留字或者容易引起歧义,建表时最好加前缀,比如sys_user、tb_user、t_medicine。如果已经用了保留字,查询时要用反引号包裹,但更推荐从源头避开。
字段类型上,时间统一用datetime,不要搞混timestamp和datetime的区别;库存数量用int就够,药品金额如果不是核心功能可以不做;有效期建议存date。主键统一用自增id,别在毕设里用UUID当主键,排序、查询、打印出来都不好看,还会被答辩老师追问。
3.2 定时提醒的三种实现方式
这是系统里最核心的“智能”体现。我在项目里尝试过三种方式,逐个说下区别。
第一种是Spring自带的@Scheduled注解,配合@EnableScheduling开启。最省事,写一个方法,用Cron表达式标注执行周期,每30秒扫一次“当前需要提醒的计划”。优点是简单、零额外依赖,缺点是单体应用内执行,适合毕设这种单服务器场景,完全够用。
第二种是Quartz框架,功能更强,支持持久化任务、集群、动态调度。如果你论文里想强调“任务调度模块使用Quartz实现”,那也说得过去。但毕设真正用到的调度复杂度,@Scheduled已经能覆盖90%,Quartz带来的复杂度反而要占用调试时间。
第三种是延迟队列/消息队列方式,比如RabbitMQ的延迟插件,理论上是大型系统的做法,但毕设里引入消息中间件会明显增加部署难度,除非你是奔着“复杂系统”去的,否则不推荐。这一点要提前想清楚,别为了让项目看起来高级而给自己添麻烦。
我实际推荐的做法是:主框架用@Scheduled做定时扫描,提醒触发后通过一个通知策略接口去分发。这样定时部分很简单,但通知部分是策略化的,接口里可以扩展短信、邮件、站内信、WebSocket等,讲起来既有层次又不失工程感。
贴一个简化版的扫描逻辑骨架:
@Component public class RemindScanTask { @Resource private MedicationPlanService planService; @Resource private NotifyDispatcher notifyDispatcher; @Scheduled(cron = "0 */1 * * * ?") public void scan() { List<MedicationPlan> plans = planService.findDuePlans(new Date()); for (MedicationPlan plan : plans) { boolean sent = notifyDispatcher.dispatch(plan); if (sent) { planService.markNotified(plan.getId(), new Date()); } } } }这个类干的活很简单:每分钟扫一次到点的计划,按照计划里的用户去分发通知。findDuePlans里查的就是“当前时间落在计划生效区间,且计划状态为启用,且该计划在当前Cron规则下到了触发点”的计划。数据库里给plan表加一个last_trigger_time字段,可以避免重复触发。这里很多人会漏掉幂等处理,不加这个字段的话,提醒可能会重复推送,用户会被烦死。
3.3 权限拦截与身份认证的实现细节
如果采用拦截器 + JWT的方案,流程大概是:
用户登录时校验账号密码,成功后签发一个JWT,设置过期时间;前端把Token存在本地,每次请求带上 Authorization 头;后端写一个拦截器,从请求头里取Token,解析用户ID和角色,放行或拒绝。
拦截器里比较容易被忽略的是Token过期处理和角色鉴权。Token过期不能只返回一个笼统的500,而应该返回401,让前端知道要跳回登录页。角色鉴权建议在拦截器里加一个针对“管理类接口”的规则检查,或者写一个@RequireAdmin注解标注在需要管理员权限的Controller方法上。
简化版角色校验可以直接写在拦截器里:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 放行登录、注册等白名单接口 // 2. 从Header取Token并解析 // 3. 校验过期时间 // 4. 如果请求路径以 /admin/ 开头,要求ROLE_ADMIN // 5. 通过则放行,否则返回401/403 }做一个@RequireAdmin注解会更好看,配合注解处理器,Controller上标注一下就生效,避免在拦截器里写一大堆路径匹配逻辑。这样的设计在论文里也好写:基于自定义注解的接口权限控制。写的时候注意:自定义注解要明确标注@Target和@Retention,别掉进注解不生效的坑。
3.4 页面上那些统计数据怎么来
首页统计、图形报表是容易被忽略但又是加分项的部分。统计口径要和真实逻辑对得上,比如“本周按时服药率”。它的计算方式是:本周实际已服用次数 ÷ 本周应服用次数,应服用次数来自服药计划,实际服用次数来自服药记录。
这类数据不适合每次请求都实时全表扫,可以在查询时做简单的聚合。拿按时服药率来说:先查用户本周所有的计划触发次数,再查用户本周已确认服药的记录数,两者一除就是比例。数据量几百上千条时,MySQL聚合查询完全没压力。
图表展示方面,比较常见的组合是后端返回JSON统计数据,前端用可视化库绘制。如果用的是Thymeleaf服务端渲染,集成ECharts很顺,引入JS文件,从后端拿数据填充折线图、饼图。如果用了Vue等前端框架,也一样,后端提供统计接口,前端渲染ECharts。这里比较推荐ECharts,文档全、效果好、免费,拿来画“近7天用药趋势”“药品过期状态分布”非常合适。
4. 调试运行与高频问题排查
4.1 拿到代码之后怎么把它跑起来
很多同学卡在第一步:代码是完整的,但本地就是启动不起来。这里给一个通用且稳定的启动流程。
第一步,检查JDK版本。Spring Boot 2.x 通常要求JDK8或JDK11,Spring Boot 3.x 要求JDK17。如果项目用的2.7但本机装了17,有些老依赖可能出问题;反过来项目用3.x但本机是8,直接启动失败。先看pom.xml里的依赖版本,再对本机JDK。
第二步,检查MySQL。建好数据库,导入SQL脚本,注意连接串里的库名、用户名、密码要和application.yml里完全一致。字符集用utf8mb4,避免中文乱码。这一步90%的启动报错都出现在这里。
第三步,检查Redis等中间件是否安装并启动。如果系统里用到缓存,但没有启动对应服务,Spring Boot启动时会连接超时。
第四步,运行启动类。先在IDE里用main方法启动,确认没有报错后再尝试打包运行。mvn clean package -DskipTests 打jar包,java -jar 运行。这一步验证了项目在纯命令行环境也能跑,答辩现场换一台机器也不慌。
4.2 我见过的高频报错和排查思路
第一个高频问题是时区导致定时任务时间不对。MySQL连接串里如果没有设置 serverTimezone,数据库返回的时间和我们本地时间可能差8小时。Spring Boot默认的JVM时区也可能不是东八区。处理方法是:连接串加serverTimezone=Asia/Shanghai,同时可以在启动类里设置TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"))。这个坑很隐蔽,系统跑着跑着发现提醒时间总是差8小时,基本就是时区问题。
第二个高频问题是LocalDateTime序列化格式不对。Spring Boot对java.time默认序列化成数组,前端拿到的是 [2025, 6, 1, 8, 0, 0] 这种结构,简直没法用。解决方法是配置Jackson的JavaTimeModule,并设置日期格式。也可以在application.yml里配置spring.jackson.date-format,但这种方式对LocalDateTime不一定生效,最靠谱是写一个Jackson配置类。
第三个高频问题是Lombok没生效。代码里用了@Data,但页面报错说找不到getter/setter。多半是IDE没有安装或启用Lombok插件,或者在编译阶段没有把Lombok作为annotation processor处理。解决办法是在pom里显式声明annotationProcessorPaths,或者IDE里勾选Enable annotation processing。
第四个高频问题是数据库保留字冲突。这个前面提到过,表名或者字段叫user、desc、schedule、order,查询SQL报语法错误,看日志会发现MySQL报了一大串语法异常。顺手把建表SQL改干净比到处加反引号省心得多。
第五个高频问题是端口被占用。8080被其他程序占用了,启动报“Port already in use”。处理方式分两种:找到占用进程杀掉,或者直接在application.yml里换一个端口,比如8081。
4.3 答辩之前一定要做的三件事
第一件事,把项目的核心业务链路完整走一遍,把演示用的数据准备好。数据不要是建表时初始化的几条,而是让人看起来像真实使用过的数据:十几个家庭、几十条药品档案、近一个月的服药记录、几条过期预警。这样打开首页的效果和空数据完全是两个画风。
第二件事,提前想好“如果提醒发送失败怎么办”的答法。这个问题几乎是必问的,因为它涉及定时任务、通知渠道和异常处理。你可以讲:通知发送前后都有日志记录,失败时会记录错误原因,并在下次扫描时重试;如果连续失败多次,系统会在站内信中追加异常提醒。这个回答体现的是你对可靠性的考虑。
第三件事,准备一个“如果数据量增大怎么办”的答案。点出当前系统在单表几千条级别可以流畅运行,如果数据量进一步增大,可以从索引优化、分页查询、缓存、读写分离、任务拆分的思路去扩展。不需要你真的做,但要说得出思路。
4.4 常见问题速查表
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
| 项目启动失败,提示数据库连接失败 | MySQL未启动或连接参数不对 | 检查服务、账号密码、库名、端口 |
| 中文乱码 | 数据库字符集不是utf8mb4 | 建库建表统一utf8mb4,连接串加characterEncoding=utf8 |
| 定时任务不执行 | 缺少@EnableScheduling | 在启动类或配置类上加上 |
| 提醒时间差8小时 | 时区问题 | 连接串加serverTimezone,代码里设置默认时区 |
| 页面显示LocalDateTime数组 | Jackson未配置 | 配置JavaTimeModule和日期格式 |
| 接口返回401/403,前端无处理 | 拦截器未放行或Token失效 | 统一返回体处理,前端捕获后跳转登录 |
| 打jar包后静态资源404 | 打包未包含静态资源或路径不对 | 确认resources目录位置和打包配置 |
5. 定制扩展方向与后续规划
5.1 从毕设到作品,怎么加亮点
基础功能做完之后,如果你还有余力,有几个扩展方向提升项目整体档次。
方向一是加消息推送多样性。前面提到的站内信已经做了,再加WebSocket实时通知,用户在线时页面可以实时弹一条“该吃药了”的提醒,这个交互效果现场演示很有冲击感。想再进一步,可以接入短信或微信服务号模板消息,这部分可以作为“预留商业部署能力”写在论文里。
方向二是做一个小程序或移动端配合页面。微信小程序端做展示和确认服药,后端提供RESTful接口。不要小看这个扩展,它把项目从“单端管理系统”升级成了“前后端+移动端”的完整业务系统,在论文里的分量完全不同。
方向三是增加用药分析算法。比如基于用户的连续未服药次数,计算依从性评分;或者分析某类疾病的用药周期;再或者根据近30天服药记录,生成一份简单的健康周报。这些不复杂,但让系统显得更智能,答辩时能讲出更细的东西。
5.2 安全和性能上的几个进阶点
基础版本满足正常使用,但我们都知道毕设答辩应对的不仅是功能演示,还有技术追问。安全和性能是两个躲不开的提问方向。
安全层面,至少要做到:密码不能明文存储,用BCrypt或SHA-256加盐;登录接口做简单限流,防止暴力破解;管理接口必须有权限校验;上传的文件要校验类型和大小。这些点哪怕只是提到了,也能体现你对安全的敏感度。
性能层面,重点说缓存和查询优化。药品列表、统计数据这类读多写少的接口,可以先用本地缓存或者加入Redis缓存。定时扫描任务里的查找计划SQL,务必保证查询条件和排序字段上有索引。分页查询统一用PageHelper或MyBatis-Plus分页插件,不要让前端一把梭把全表数据拉走。
有些同学喜欢在答辩前临时加一堆“高并发”“分布式”的关键词,我个人不太建议。毕设项目的核心是逻辑完整、技术自洽、能讲清楚。你用了Redis,就要能讲清楚缓存了什么、什么时候失效;你提了RabbitMQ,就要能讲清楚消息在哪发、在哪收、丢了怎么处理。技术栈要“用得上、讲得明”,而不是“堆得多、问不得”。
最后分享一点我个人在带这类项目过程中的体会:智能药箱这个题目不属于那种“一眼惊艳”的类型,但它是典型的能力放大器。你花多少心思在业务细节上,答辩时就能呈现多少东西。哪怕是简单的过期提醒逻辑,你把“到期前一天怎么提醒、当天怎么提醒、过期了怎么标记”这三层讲透,也比只会背概念强得多。
我见过不少同学在定时任务这里翻车,原因不是不会写@Scheduled,而是没有处理重复触发、没有考虑时区、没有记录通知结果。这些问题在学校里的教程中很少被提到,但你真正去做的时候一定会碰到。把这些问题提前解决掉,你的项目就不是“能跑”,而是“抗问”。
如果你选了这个题目,我建议从功能拆分开始动手,先把业务链路理清楚,再逐块实现。把这个系统做扎实,既完成了一次完整的Java后端训练,也留下了一个可以在简历上写的作品,一举两得。